@jskit-ai/architecture-inspector 0.0.0-stage → 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,3 +1,175 @@
1
- # Temporary Holding Version
1
+ # Architecture Inspector
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ `@jskit-ai/architecture-inspector/vue` exports
4
+ `inspectVueArchitecture({ projectRoot, input, aliases, httpScope, apiBasePath, calls, apiPrefixAliases })`
5
+ for finite static architecture inspection, and ships the `jskit-inspect-vue`
6
+ command and the Vue Preview locator `src/previewLocator.js`. It is a plain tool
7
+ package, not a JSKIT framework runtime package.
8
+
9
+ ## Architecture inspection
10
+
11
+ Imported assets remain file-import evidence. The Vue adapter parses only its
12
+ supported JavaScript, TypeScript and Vue modules; binary images do not block
13
+ route discovery. Asset-to-component transformations require their own adapter
14
+ and remain explicitly unsupported here.
15
+
16
+ Run `jskit-inspect-vue` in the project directory. It reads one bounded JSON
17
+ document from stdin and writes one `genesis.architecture-observations.v1` JSON
18
+ document to stdout. Errors use stderr and a failing exit status. It does not
19
+ import application code, start a server, generate router declarations or retain
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.
25
+
26
+ Input supplies `schema: "vibe64.architecture-inspection-input.v1"`, indexed
27
+ `files: [{ path, hash }]` and `uiDiscovery.discoveries`. Optional `packages`
28
+ carry `id`, `name`, `manifest`, `declarations`, `root`, `opaque` and public
29
+ `entryPoints: [{ importPath, file }]`; `packageIdentity` binds observations to
30
+ the same inspected public boundaries. Paths are project-relative
31
+ and hashes use `sha256:<hex>`. Only declared, hash-matching UTF-8 files are read;
32
+ symlinks and files over 4 MiB are rejected. Optional `stackHash` and
33
+ `cacheDirectory` serve the call cache below; other unknown fields are ignored.
34
+
35
+ Each discovery supplies `id`, `adapter`, `surface`, `mode`, `registrations`,
36
+ `pageRoots` and `componentRoots`. Supported adapters are `jskit`,
37
+ `vue-registered-router` and `vue-filesystem-router`.
38
+
39
+ - Registered mode reads literal Vue Router `createRouter({ routes })` declarations
40
+ from the registration files. Static local arrays and literal child routes work.
41
+ - Filesystem mode reads literal `VueRouter(...)` options from the registration
42
+ files and uses Vue Router's own scanner with declarations and watchers disabled.
43
+ Its actual `routesFolder` determines pages; a directory alone is not proof.
44
+ Sibling routes are visited in `fullPath` order, so registration ids do not
45
+ depend on filesystem scan order.
46
+ - Template component uses come from actual referenced imports. Shared definitions
47
+ retain their source identity. Unused imports create no composition relationship.
48
+ - Imported external elements retain the declared package/import-path/symbol owner.
49
+ Their implementation files are not read. Project-owned workspace exports can
50
+ resolve to indexed source files.
51
+ - Static imports from indexed JavaScript, TypeScript and Vue files also provide
52
+ optional `imports` observations. The framework resolver owns declared aliases,
53
+ root-relative paths and file resolution. Each record retains the source range,
54
+ current input hashes, target file and imported names, including aliases,
55
+ `default`, namespace `*` and type-only classification. These are import facts,
56
+ not evidence of runtime calls. Coverage remains partial; namespace use does
57
+ not establish which individual exports are consumed.
58
+ - An HTTP scope names the application's operations and enables scoped literal
59
+ `fetch` uses and receivers registered through JSKIT's `defineFeature` setup
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.
64
+ - Actual `CrudListScreen` / `CrudAddEditScreen` uses whose bound screen comes from
65
+ the matching JSKIT composable can connect to exported `defineCrudJsonApiFeature`
66
+ controller declarations. The resource must resolve through indexed imports and
67
+ named re-exports to a literal `defineCrudResource` namespace and enabled
68
+ operations. Scope includes the explicit HTTP application identity, declared
69
+ surface and API suffix; the key uses `jskit.crud` resource operations rather
70
+ than claiming a fully expanded runtime URL. Custom clients, transport/resource
71
+ overrides, dynamic operations and disabled routes do not create guessed links.
72
+ This shows declared entry points; runtime provider activation is not proven.
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
+
153
+ Dynamic routing, template component tokens, runtime placement predicates,
154
+ application request wrappers and dispatcher conventions require more evidence or
155
+ an application-owned inspector. They remain partial or unsupported, never
156
+ guessed. This command does not claim complete runtime call analysis. Protocols
157
+ other than HTTP can use the same observation contract through their own inspector.
158
+
159
+ Example consumer-owned Stack operation:
160
+
161
+ ```markdown
162
+
163
+ ### Inspector `vue`: Inspect Vue pages and function calls
164
+
165
+ - Runtime: `nodejs`
166
+ - Directory: `.`
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`
171
+ ```
172
+
173
+ The consumer owns command admission, runtime selection, cancellation, publication
174
+ and validation against the indexed snapshot. It must retain the last valid
175
+ snapshot when an inspector fails or source changes.
package/package.json CHANGED
@@ -1,6 +1,39 @@
1
1
  {
2
2
  "name": "@jskit-ai/architecture-inspector",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Static Vue architecture inspector for the portable architecture-inspection command contract.",
5
+ "type": "module",
6
+ "files": [
7
+ "src"
8
+ ],
9
+ "scripts": {
10
+ "test": "node --test --test-concurrency=1"
11
+ },
12
+ "exports": {
13
+ "./vue": "./src/inspectVueArchitecture.js",
14
+ "./cli": "./src/cli.js"
15
+ },
16
+ "bin": {
17
+ "jskit-inspect-vue": "./src/cli.js"
18
+ },
19
+ "dependencies": {
20
+ "@babel/parser": "7.29.9",
21
+ "@jskit-ai/kernel": "0.2.3",
22
+ "@vue/compiler-sfc": "3.5.44",
23
+ "typescript": "6.0.3"
24
+ },
25
+ "peerDependencies": {
26
+ "vue-router": "^5.3.1"
27
+ },
28
+ "engines": {
29
+ "node": "^22.13.0 || ^24.0.0 || ^26.0.0"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "keywords": [
35
+ "jskit",
36
+ "vue",
37
+ "architecture"
38
+ ]
39
+ }
@@ -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 };