@jskit-ai/architecture-inspector 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -1,9 +1,10 @@
1
1
  # Architecture Inspector
2
2
 
3
3
  `@jskit-ai/architecture-inspector/vue` exports
4
- `inspectVueArchitecture({ projectRoot, input, aliases, httpScope, apiBasePath })`
4
+ `inspectVueArchitecture({ projectRoot, input, aliases, httpScope, apiBasePath, calls, apiPrefixAliases })`
5
5
  for finite static architecture inspection, and ships the `jskit-inspect-vue`
6
- command. It is a plain tool package, not a JSKIT framework runtime package.
6
+ command and the Vue Preview locator `src/previewLocator.js`. It is a plain tool
7
+ package, not a JSKIT framework runtime package.
7
8
 
8
9
  ## Architecture inspection
9
10
 
@@ -16,15 +17,25 @@ Run `jskit-inspect-vue` in the project directory. It reads one bounded JSON
16
17
  document from stdin and writes one `genesis.architecture-observations.v1` JSON
17
18
  document to stdout. Errors use stderr and a failing exit status. It does not
18
19
  import application code, start a server, generate router declarations or retain
19
- a watcher. The package has no Genesis or Vibe64 dependency.
20
+ a watcher. The package imports no consumer's code: the input contract is its
21
+ own, and it writes the observation format its consumer defines, which it names
22
+ only by schema id. The command runs the
23
+ inspection in a child Node process whose old generation is bounded by the size
24
+ of the declared scripts (192 MB plus 56 MB per MiB), so memory stays near what
25
+ the inspection holds; a caller that starts Node with its own
26
+ `--max-old-space-size` keeps that bound and runs in place.
20
27
 
21
- Input supplies `schema: "vibe64.architecture-inspection-input.v1"`, indexed
28
+ Input supplies `schema: "jskit.architecture-inspection-input.v1"`, indexed
22
29
  `files: [{ path, hash }]` and `uiDiscovery.discoveries`. Optional `packages`
23
30
  carry `id`, `name`, `manifest`, `declarations`, `root`, `opaque` and public
24
31
  `entryPoints: [{ importPath, file }]`; `packageIdentity` binds observations to
25
- the same inspected public boundaries. Paths are project-relative
32
+ the same inspected public boundaries. A package resolves for the files under
33
+ each manifest that declares it (`declarations: [{ manifest }]`, or its
34
+ `manifest` when it has none), whatever the dependency kind, so the packages of
35
+ a workspace resolve one another when each workspace manifest is declared. Paths are project-relative
26
36
  and hashes use `sha256:<hex>`. Only declared, hash-matching UTF-8 files are read;
27
- symlinks and files over 4 MiB are rejected.
37
+ symlinks and files over 4 MiB are rejected. Optional `stackHash` and
38
+ `cacheDirectory` serve the call cache below; other unknown fields are ignored.
28
39
 
29
40
  Each discovery supplies `id`, `adapter`, `surface`, `mode`, `registrations`,
30
41
  `pageRoots` and `componentRoots`. Supported adapters are `jskit`,
@@ -49,9 +60,21 @@ Each discovery supplies `id`, `adapter`, `surface`, `mode`, `registrations`,
49
60
  `default`, namespace `*` and type-only classification. These are import facts,
50
61
  not evidence of runtime calls. Coverage remains partial; namespace use does
51
62
  not establish which individual exports are consumed.
52
- - An explicit `--http-scope shop --api-base-path /api` enables scoped literal
53
- `fetch` uses and receivers registered through JSKIT's `defineFeature` setup
54
- `http.router.register`. Use `--alias @=src` for declared project aliases.
63
+ - An HTTP scope names the application's operations and enables scoped literal
64
+ `fetch` uses and the routes a `defineFeature` setup registers on its HTTP
65
+ router. It is the root `package.json` `name`, or `--http-scope shop` when
66
+ the operations should carry another name; without either, HTTP observations
67
+ stay unconfigured. Use `--alias @=src` for declared project aliases.
68
+ - `router.register(method, path, options, handler)` becomes an entry operation
69
+ whose controller is the handler, when the router is `http.router` of the
70
+ setup's destructured `http` dependency (or `dependencies.http.router`), used
71
+ in the setup or passed to a project function that registers on the parameter
72
+ receiving it. Such functions may be local or imported through named imports
73
+ and re-exports, at most four calls deep. The method and path must be
74
+ literals, local constants or template literals of them; a call of the
75
+ kernel's `resolveScopedApiBasePath` with literal options stands for the path
76
+ it returns. Anything else is reported as `CONTROLLER_UNRESOLVED`.
77
+ `--api-base-path /api` prefixes these paths.
55
78
  - Actual `CrudListScreen` / `CrudAddEditScreen` uses whose bound screen comes from
56
79
  the matching JSKIT composable can connect to exported `defineCrudJsonApiFeature`
57
80
  controller declarations. The resource must resolve through indexed imports and
@@ -61,6 +84,104 @@ Each discovery supplies `id`, `adapter`, `surface`, `mode`, `registrations`,
61
84
  than claiming a fully expanded runtime URL. Custom clients, transport/resource
62
85
  overrides, dynamic operations and disabled routes do not create guessed links.
63
86
  This shows declared entry points; runtime provider activation is not proven.
87
+ Each such entry operation carries the `actionId` its route runs,
88
+ `crud.<namespace>.<operation>`. The declaration's custom `actions` (a literal
89
+ object, bound locally or imported through named re-exports) that declare a
90
+ literal `route` become `http` entry operations at the declaration's API base
91
+ path (`scope.routeBase` and `relativePath`) plus the route's `path`, with the
92
+ action's `id`, or `crud.<namespace>.<name>` without one. Other forms are reported
93
+ as `CRUD_ACTION_ROUTE_UNRESOLVED`.
94
+
95
+ - Routes registered through the kernel's `createActionRouteScope` become entry
96
+ operations with their full path and, for `actionRoute`, their `actionId`.
97
+ The scope options must be string literals or imported constant strings;
98
+ each route's method, suffix and `actionId` must be literals, template
99
+ literals or elements of an enclosing literal array's `.map` or `for...of`
100
+ (at most 64). Anything else is reported as `ACTION_ROUTE_UNRESOLVED` or
101
+ `ACTION_ROUTE_SCOPE_UNBOUND`, never guessed.
102
+
103
+ ### Function calls (`--calls`)
104
+
105
+ `--calls` (library option `calls: true`) adds `functions`, `calls` and
106
+ `entryPoints` and their capabilities. Without it the output is unchanged.
107
+
108
+ - **Functions.** Every function-like with a body, plus each file's top-level
109
+ code as `fn:<path>#module`. Other ids are `fn:<path>#<line>:<column>` at the
110
+ syntax node's start after decorators, `export` and comments, the same
111
+ position Genesis's syntax sites use. `exported: true` marks exported
112
+ top-level functions; `container` names an enclosing class or object.
113
+ - **Calls.** One TypeScript program over the declared scripts and Vue script
114
+ blocks only, plus TypeScript's own libraries; nothing under `node_modules` is
115
+ loaded. Each call is `exact` (`checker`, `di-binding`, `self`,
116
+ `route-action` or single-valued `argument-flow`), `probable` (other
117
+ `argument-flow`, `receiver-name`, or several candidates) or `unresolved`.
118
+ - `defineFeature` injection binds `requires` tokens to the `setup` outputs
119
+ that `provides` them: one provider is exact, several are ambiguous.
120
+ - Argument flow follows the caller. A function passed to a parameter that
121
+ the callee calls while it runs (in its own body, in callbacks it passes
122
+ on, or by forwarding it to a function that does) is an `argument-flow`
123
+ call of the function that passes it, recorded at that call. The callee's
124
+ own call of the parameter is `unresolved`, so one caller's callback is
125
+ never reached through another caller. A parameter called only from a
126
+ closure the callee stores or returns keeps every value its callers pass.
127
+ Argument flow is exact when it names one function for certain: a passed
128
+ inline function or a name bound to one function, or a parameter (also a
129
+ destructured factory parameter such as `createActions({ sessions })`)
130
+ that receives one value across all the project's calls, when that value
131
+ resolves exactly. Several values stay probable.
132
+ - JSDoc types are declared types: `@param`, `@type`, `@typedef` and
133
+ `import("...")` types, including a package's `./server/types` reached
134
+ through its declared entry points. A call through a parameter or binding
135
+ typed that way is a `checker` call. A member a type declares as `typeof` a
136
+ function (`{ run: typeof run }`, `@property {typeof run} run`) is that
137
+ function.
138
+ - Action definitions (`{ id, execute }` in a feature's `actions`, also through
139
+ `create*Actions` factories, pass-through wrappers and literal-array `.map`,
140
+ with ids that are literals or module constants) and the custom actions of a
141
+ CRUD declaration become `action` entry points;
142
+ `request.executeAction({ actionId: "..." })` reaches their execute
143
+ function, and the `controllerFunctionId` of each entry operation that names
144
+ an action is that function.
145
+ - Calls that are provably external are left out: package imports, builtins,
146
+ values produced by them (including `let` assignments, `for...of`,
147
+ destructuring and callbacks they call), Vue's `<script setup>` macros and
148
+ `emit`.
149
+ - A method call on an untyped receiver (`receiver-name`) reaches a project
150
+ function only when exactly one function on that side declares the name
151
+ (or exactly one in a file the receiver names) and no builtin member has
152
+ it: the members of TypeScript's libraries and of Node's `EventEmitter`,
153
+ `fs.Stats` and streams. Otherwise it is `unresolved`, and a builtin name
154
+ no project function declares is left out.
155
+ - At most 2,000 call records per file; beyond that `CALLS_TRUNCATED` and a
156
+ `partial` calls capability.
157
+ - **Transports.** With an HTTP scope, `useEndpointResource`, `useCommand`,
158
+ `getHttpWebClient().request` and client `fetch` become operation uses that
159
+ carry `fromFunctionId`; `parentId` is set only in page and component files.
160
+ Paths are partially evaluated (template literals, constants, imports,
161
+ parameter defaults, `usePaths().api`, `resolveScopedApiBasePath`). A key is
162
+ `keyCertainty: "exact"` when every route literal is matched; two or three
163
+ candidate routes give one `probable` use each, with ids suffixed per
164
+ candidate. `--api-prefix-alias /api/x=/api/app/:slug/x` (repeatable) rewrites
165
+ client prefixes before matching.
166
+ - **Cache.** With an absolute, existing `cacheDirectory` outside the project,
167
+ the command keeps `jskit-calls.json` there. An unchanged rerun reuses the
168
+ whole result. Otherwise a file keeps its calls when its own and its directly
169
+ imported sources are unchanged and every target still exists. Calls that
170
+ injection or argument flow resolve depend on other files' arguments, so they
171
+ are resolved again on every run; a file whose such calls now reach other
172
+ functions is resolved again, with every reused file whose calls reach it. Any change to a
173
+ manifest, lockfile, `tsconfig`/`jsconfig`, the inspector's own manifest or
174
+ source (even without a version bump), packages, UI discovery, options or
175
+ `stackHash` rebuilds everything. An absent, unusable or corrupt cache means a
176
+ cold run.
177
+
178
+ ### Preview locator
179
+
180
+ `src/previewLocator.js` is a classic script a Preview can inline. It defines
181
+ `globalThis.__jskitLocateElement(element)`, which returns `{ file, name? }` for
182
+ the nearest component with a source file (Vue development builds record it as
183
+ `__file`) that rendered the element, or `null` for plain DOM. It reads only
184
+ Vue's component bookkeeping, never text content or values.
64
185
 
65
186
  Dynamic routing, template component tokens, runtime placement predicates,
66
187
  application request wrappers and dispatcher conventions require more evidence or
@@ -72,13 +193,14 @@ Example consumer-owned Stack operation:
72
193
 
73
194
  ```markdown
74
195
 
75
- ### Inspector `vue`: Inspect Vue pages
196
+ ### Inspector `vue`: Inspect Vue pages and function calls
76
197
 
77
198
  - Runtime: `nodejs`
78
199
  - Directory: `.`
79
- - Command: `node` `node_modules/@jskit-ai/architecture-inspector/src/cli.js` `--alias` `@=src`
80
- - Timeout seconds: `30`
81
- - Max output bytes: `4194304`
200
+ - Command: `node` `node_modules/@jskit-ai/architecture-inspector/src/cli.js` `--alias` `@=src` `--calls`
201
+ - Timeout seconds: `90`
202
+ - Max output bytes: `67108864`
203
+ - Preview locator: `node_modules/@jskit-ai/architecture-inspector/src/previewLocator.js`
82
204
  ```
83
205
 
84
206
  The consumer owns command admission, runtime selection, cancellation, publication
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/architecture-inspector",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Static Vue architecture inspector for the portable architecture-inspection command contract.",
5
5
  "type": "module",
6
6
  "files": [
@@ -18,7 +18,9 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "@babel/parser": "7.29.9",
21
- "@vue/compiler-sfc": "3.5.44"
21
+ "@jskit-ai/kernel": "0.2.4",
22
+ "@vue/compiler-sfc": "3.5.44",
23
+ "typescript": "6.0.3"
22
24
  },
23
25
  "peerDependencies": {
24
26
  "vue-router": "^5.3.1"
@@ -0,0 +1,97 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync, readdirSync } from "node:fs";
3
+ import { readFile, rename, stat, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ // v3: each file keeps its checker sites, the candidates injection or argument
8
+ // flow can resolve, and the sites they resolved to, apart.
9
+ const CACHE_SCHEMA = "jskit.architecture-calls-cache.v3";
10
+ const CACHE_FILE = "jskit-calls.json";
11
+ // Manifests, lockfiles and compiler configurations change how every file
12
+ // resolves, so any change to them rebuilds everything.
13
+ const PROJECT_FILE_PATTERN = /(?:^|\/)(?:package\.json|package-lock\.json|npm-shrinkwrap\.json|yarn\.lock|pnpm-lock\.yaml|[jt]sconfig(?:\.[\w.-]+)?\.json)$/u;
14
+ const INSPECTOR_ROOT = fileURLToPath(new URL("../../", import.meta.url));
15
+
16
+ function digest(value) {
17
+ return createHash("sha256").update(JSON.stringify(value)).digest("hex");
18
+ }
19
+
20
+ // The inspector's manifest (its version and dependency pins) and every source
21
+ // file, read once per run. A changed resolver changes the key even without a
22
+ // version bump, as in source development or a linked checkout.
23
+ let inspectorIdentity = null;
24
+ function inspectorSourceHash() {
25
+ if (!inspectorIdentity) {
26
+ const hash = createHash("sha256").update(readFileSync(path.join(INSPECTOR_ROOT, "package.json")));
27
+ const sourceRoot = path.join(INSPECTOR_ROOT, "src");
28
+ for (const file of readdirSync(sourceRoot, { recursive: true }).filter((name) => name.endsWith(".js")).sort()) {
29
+ hash.update(`\0${file}\0`).update(readFileSync(path.join(sourceRoot, file)));
30
+ }
31
+ inspectorIdentity = hash.digest("hex");
32
+ }
33
+ return inspectorIdentity;
34
+ }
35
+
36
+ // The cache file inside the caller's cache directory, or null when the
37
+ // directory is not an existing absolute directory outside the project.
38
+ async function cacheLocation(projectRoot, directory) {
39
+ if (typeof directory !== "string" || !path.isAbsolute(directory)) {
40
+ return null;
41
+ }
42
+ const resolved = path.resolve(directory);
43
+ if (resolved === projectRoot || resolved.startsWith(`${projectRoot}${path.sep}`)) {
44
+ return null;
45
+ }
46
+ const entry = await stat(resolved).catch(() => null);
47
+ return entry?.isDirectory() ? path.join(resolved, CACHE_FILE) : null;
48
+ }
49
+
50
+ async function readCallCache(location, key) {
51
+ try {
52
+ const cache = JSON.parse(await readFile(location, "utf8"));
53
+ return cache?.schema === CACHE_SCHEMA && cache.key === key && cache.files && typeof cache.files === "object" ? cache : null;
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+
59
+ // Written beside the final file and renamed into place; a cache that cannot
60
+ // be written only means the next run is cold.
61
+ async function writeCallCache(location, cache) {
62
+ const temporary = `${location}.${process.pid}.tmp`;
63
+ try {
64
+ await writeFile(temporary, JSON.stringify({ schema: CACHE_SCHEMA, ...cache }));
65
+ await rename(temporary, location);
66
+ } catch {
67
+ // An unusable cache directory leaves the inspection result unchanged.
68
+ }
69
+ }
70
+
71
+ // Keys for one inspection: everything that affects every file, each file's
72
+ // own and directly imported sources, and the whole run.
73
+ function cacheKeys({ input, files, imports, scripts, options }) {
74
+ const projectFiles = [...files.values()]
75
+ .filter((file) => PROJECT_FILE_PATTERN.test(file.path))
76
+ .map((file) => [file.path, file.hash])
77
+ .sort();
78
+ const key = digest({
79
+ inspector: inspectorSourceHash(),
80
+ stackHash: input.stackHash ?? null,
81
+ packages: input.packages ?? null,
82
+ uiDiscovery: input.uiDiscovery ?? null,
83
+ options,
84
+ projectFiles
85
+ });
86
+ const importsByFile = new Map();
87
+ for (const use of imports) {
88
+ importsByFile.set(use.fromFile, (importsByFile.get(use.fromFile) || new Set()).add(use.targetFile));
89
+ }
90
+ const fileKeys = new Map(scripts.map((file) => [file, digest([
91
+ files.get(file).hash,
92
+ [...(importsByFile.get(file) || [])].sort().map((target) => [target, files.get(target)?.hash ?? null])
93
+ ])]));
94
+ return { key, fileKeys, runKey: digest([key, [...fileKeys].sort()]) };
95
+ }
96
+
97
+ export { cacheKeys, cacheLocation, readCallCache, writeCallCache };