@jskit-ai/architecture-inspector 0.1.0 → 0.1.1

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