@kudzujs/core 0.16.11 → 0.16.13
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/MIGRATION_ROADMAP.md +1 -1
- package/PERFORMANCE.md +24 -0
- package/README.md +1 -1
- package/RELEASES.md +63 -0
- package/bin/kudzu.mjs +26 -6
- package/docs/next-architecture/README.md +1 -1
- package/docs/next-architecture/application-capability-release-plan.md +40 -0
- package/docs/next-architecture/compiler-current-architecture.md +3 -2
- package/docs/next-architecture/large-application-ai-native-roadmap.md +3 -3
- package/docs/next-architecture/versioning.md +2 -2
- package/framework/build.mjs +37 -7
- package/framework/compiler/explanation-report.mjs +137 -0
- package/framework/compiler/inspection-report.mjs +156 -0
- package/package.json +1 -1
package/MIGRATION_ROADMAP.md
CHANGED
|
@@ -57,7 +57,7 @@ Syntax compatibility does not mean reproducing React wholesale. Kudzu accepts th
|
|
|
57
57
|
- stronger JavaScript failure resilience;
|
|
58
58
|
- no remaining material large-route build-scaling weakness.
|
|
59
59
|
|
|
60
|
-
The completed 0.9 plan defines the cross-framework comparison and release gate. The active application packet is `0.20.
|
|
60
|
+
The completed 0.9 plan defines the cross-framework comparison and release gate. The active application packet is `0.20.3` in the capability release plan below. Benchmark-only feature omission, unmatched accessibility, weighted scores that hide losses, and unrecorded environment differences do not count as proof.
|
|
61
61
|
|
|
62
62
|
### 0.10.0 Through 0.21.x: Application Capability Release Train
|
|
63
63
|
|
package/PERFORMANCE.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
Reproducibility classes: `npm run benchmark`, `npm run benchmark:keyed`, `npm run benchmark:native`, `npm run benchmark:module-cache`, `npm run benchmark:project-navigation`, `npm run benchmark:project-state`, and `npm run benchmark:source-scale` are maintained in this repository; `npm run benchmark:commerce` is a maintained paired runner over the public external storefront; older excluded-workspace sections are historical provenance only and are not current framework rankings.
|
|
4
4
|
|
|
5
|
+
## 0.16.13 Route Artifact Explanations
|
|
6
|
+
|
|
7
|
+
Measured 2026-09-01 on Linux x64 with Node 24.14.0. Route explanation is a
|
|
8
|
+
build-time projection over existing route, source, compatibility, ownership,
|
|
9
|
+
capability, and artifact records. Representative deploy manifests and hashes
|
|
10
|
+
remain unchanged. The maintained Worker graph remains 907 raw / 477 gzip B and
|
|
11
|
+
the window graph remains 14,456 raw / 6,160 gzip B. Seven clean builds record a
|
|
12
|
+
605 ms median on this host without a timing comparison. Browser-disabled and
|
|
13
|
+
required-Chrome suites pass 296/296 tests, package smoke passes, and focused CLI
|
|
14
|
+
coverage includes an effect-owned Worker route, configured base, exact missing
|
|
15
|
+
route diagnostic, deterministic output, artifact hashes, and zero-JavaScript
|
|
16
|
+
static exclusion.
|
|
17
|
+
|
|
18
|
+
## 0.16.12 Bounded Application Inspection
|
|
19
|
+
|
|
20
|
+
Measured 2026-08-31 on Linux x64 with Node 24.14.0. Inspection is a build-time
|
|
21
|
+
projection over existing source, compatibility, ownership, capability, and
|
|
22
|
+
artifact records. Representative deploy manifests and hashes remain unchanged.
|
|
23
|
+
The maintained Worker graph remains 907 raw / 477 gzip B and the window graph
|
|
24
|
+
remains 14,456 raw / 6,160 gzip B. Seven clean builds record a 648.7 ms median
|
|
25
|
+
on this host without a timing comparison. Browser-disabled and required-Chrome
|
|
26
|
+
suites pass 295/295 tests, package smoke passes, and bounded deterministic CLI
|
|
27
|
+
coverage includes reachable-source filtering and structured blocker output.
|
|
28
|
+
|
|
5
29
|
## 0.16.11 Structured Compiler Diagnostics
|
|
6
30
|
|
|
7
31
|
Measured 2026-08-31 on Linux x64 with Node 24.14.0. Structured diagnostics are
|
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Kudzu compiles ordinary React-shaped TypeScript and TSX into complete static HTM
|
|
|
14
14
|
|
|
15
15
|
> Experimental `0.16.x`: the compiler API and supported TSX surface may change.
|
|
16
16
|
|
|
17
|
-
**Latest release: 0.16.
|
|
17
|
+
**Latest release: 0.16.13 - Route artifact explanations.** `kudzu explain --route <route> --json` now traces one exact emitted route from authored source and semantic ownership through its capability family and selected runtime, handler, chunk, style, and Worker bytes without exposing raw IR. Read the [release notes](./RELEASES.md#01613---route-artifact-explanations), open the [release page](https://github.com/kudzujs/kudzu/releases/tag/v0.16.13), or follow the [architecture packet](./docs/next-architecture/README.md).
|
|
18
18
|
|
|
19
19
|
- [Documentation](https://kudzujs.cloud/docs)
|
|
20
20
|
- [Installation guide](https://kudzujs.cloud/docs#install)
|
package/RELEASES.md
CHANGED
|
@@ -1,5 +1,68 @@
|
|
|
1
1
|
# Kudzu Releases
|
|
2
2
|
|
|
3
|
+
## 0.16.13 - Route Artifact Explanations
|
|
4
|
+
|
|
5
|
+
Kudzu 0.16.13 lets tools ask why one exact emitted route owns its browser bytes
|
|
6
|
+
without reading raw semantic records or reconstructing an artifact graph.
|
|
7
|
+
|
|
8
|
+
### Changed in 0.16.13
|
|
9
|
+
|
|
10
|
+
- Adds `kudzu explain --route <route> --json`, which runs the authoritative build
|
|
11
|
+
and projects authored source, normalization provenance, semantic owners,
|
|
12
|
+
capability facts, selected artifacts, and byte reasons for one emitted route.
|
|
13
|
+
- Reports raw and gzip byte totals plus SHA-256 hashes for runtime, handler,
|
|
14
|
+
transitive chunk, stylesheet, and Worker artifacts already selected by the
|
|
15
|
+
existing route artifact report.
|
|
16
|
+
- Explains zero-JavaScript routes from an absent behavior runtime family and an
|
|
17
|
+
empty compiler-selected JavaScript closure.
|
|
18
|
+
- Uses exact configured-base route matching and returns the structured
|
|
19
|
+
`explain.route.not-found` diagnostic for missing routes.
|
|
20
|
+
- Sorts every bounded section before truncation and excludes HTML, generated
|
|
21
|
+
code, RouteIR, ModuleIR, captures, and state values.
|
|
22
|
+
- Adds no analyzer, semantic primitive, compiler pass, runtime concept, public
|
|
23
|
+
application API, or browser byte.
|
|
24
|
+
- Updates `create-kudzu@0.1.138` to generate projects on
|
|
25
|
+
`@kudzujs/core@^0.16.13`.
|
|
26
|
+
|
|
27
|
+
### Upgrade
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
npm install @kudzujs/core@^0.16.13
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 0.16.12 - Bounded Application Inspection
|
|
34
|
+
|
|
35
|
+
Kudzu 0.16.12 lets tools select the first migration blocker from a compact
|
|
36
|
+
reachable-application inventory instead of reading the compiler's raw semantic
|
|
37
|
+
records or scanning an entire source tree.
|
|
38
|
+
|
|
39
|
+
### Changed in 0.16.12
|
|
40
|
+
|
|
41
|
+
- Adds `kudzu inspect --json`, which runs the authoritative build graph and emits
|
|
42
|
+
one versioned report of reachable modules, routes, package compatibility,
|
|
43
|
+
capability families, semantic owners, and blocker candidates.
|
|
44
|
+
- Sorts every section before applying fixed limits and reports total and omitted
|
|
45
|
+
counts so large applications retain bounded deterministic context.
|
|
46
|
+
- Distinguishes confirmed structured diagnostic blockers from unsupported
|
|
47
|
+
compatibility sites and partial-package review candidates.
|
|
48
|
+
- Excludes unreachable source, generated module code, HTML, RouteIR, ModuleIR,
|
|
49
|
+
captures, state values, and complete artifact closures from inspection output.
|
|
50
|
+
- Reuses existing source, compatibility, ownership, capability, and artifact
|
|
51
|
+
records; it adds no analyzer, semantic primitive, runtime concept, public
|
|
52
|
+
application API, or browser byte.
|
|
53
|
+
- Updates `create-kudzu@0.1.137` to generate projects on
|
|
54
|
+
`@kudzujs/core@^0.16.12`.
|
|
55
|
+
|
|
56
|
+
`inspect` performs the ordinary authoritative build, including configured
|
|
57
|
+
`afterBuild()` behavior. A structured compiler failure returns a bounded blocked
|
|
58
|
+
report with incomplete inventory rather than pretending that later facts exist.
|
|
59
|
+
|
|
60
|
+
### Upgrade
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
npm install @kudzujs/core@^0.16.12
|
|
64
|
+
```
|
|
65
|
+
|
|
3
66
|
## 0.16.11 - Structured Compiler Diagnostics
|
|
4
67
|
|
|
5
68
|
Kudzu 0.16.11 gives tools and agents stable machine-readable authored-source
|
package/bin/kudzu.mjs
CHANGED
|
@@ -6,19 +6,39 @@ import { fileURLToPath } from "node:url"
|
|
|
6
6
|
|
|
7
7
|
const command = process.argv[2] ?? "dev"
|
|
8
8
|
|
|
9
|
-
if (command === "build" && !process.env.KUDZU_BUILD_CHILD) {
|
|
9
|
+
if ((command === "build" || command === "inspect" || command === "explain") && !process.env.KUDZU_BUILD_CHILD) {
|
|
10
10
|
const child = spawnSync(process.execPath, ["--expose-gc", "--max-semi-space-size=8", fileURLToPath(import.meta.url), ...process.argv.slice(2)], {
|
|
11
11
|
stdio: "inherit",
|
|
12
12
|
env: { ...process.env, KUDZU_BUILD_CHILD: "1" }
|
|
13
13
|
})
|
|
14
14
|
if (child.error) throw child.error
|
|
15
15
|
process.exitCode = child.status ?? 1
|
|
16
|
-
} else if (command === "build" || command === "dev") {
|
|
16
|
+
} else if (command === "build" || command === "dev" || command === "inspect" || command === "explain") {
|
|
17
17
|
module.enableCompileCache?.()
|
|
18
|
-
const { build, dev } = await import("../framework/build.mjs")
|
|
19
|
-
const json = command === "build" && process.argv.includes("--json")
|
|
18
|
+
const { build, dev, explain, inspect } = await import("../framework/build.mjs")
|
|
19
|
+
const json = (command === "build" || command === "explain") && process.argv.includes("--json")
|
|
20
20
|
try {
|
|
21
|
-
|
|
21
|
+
if (command === "inspect" || command === "explain") {
|
|
22
|
+
const args = process.argv.slice(3)
|
|
23
|
+
const routeIndex = args.indexOf("--route")
|
|
24
|
+
const route = routeIndex === -1 ? undefined : args[routeIndex + 1]
|
|
25
|
+
const validExplain = command !== "explain" || args.length === 3 && args.filter(arg => arg === "--json").length === 1 && args.filter(arg => arg === "--route").length === 1 && route && !route.startsWith("--") && args.every((arg, index) => arg === "--json" || arg === "--route" || index === routeIndex + 1)
|
|
26
|
+
if (!process.argv.includes("--json")) {
|
|
27
|
+
console.error(command === "inspect" ? "Use: kudzu inspect --json" : "Use: kudzu explain --route <route> --json")
|
|
28
|
+
process.exitCode = 1
|
|
29
|
+
} else if (!validExplain) {
|
|
30
|
+
console.error("Use: kudzu explain --route <route> --json")
|
|
31
|
+
process.exitCode = 1
|
|
32
|
+
} else {
|
|
33
|
+
const log = console.log
|
|
34
|
+
console.log = console.error
|
|
35
|
+
try {
|
|
36
|
+
process.stdout.write(`${JSON.stringify(await (command === "inspect" ? inspect() : explain({ route })))}\n`)
|
|
37
|
+
} finally {
|
|
38
|
+
console.log = log
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
} else await (command === "build" ? build({ quiet: json }) : dev())
|
|
22
42
|
} catch (error) {
|
|
23
43
|
const { diagnosticEnvelope } = await import("../framework/compiler/diagnostics.mjs")
|
|
24
44
|
const envelope = diagnosticEnvelope(error)
|
|
@@ -27,6 +47,6 @@ if (command === "build" && !process.env.KUDZU_BUILD_CHILD) {
|
|
|
27
47
|
process.exitCode = 1
|
|
28
48
|
}
|
|
29
49
|
} else {
|
|
30
|
-
console.error(`Unknown command: ${command}\nUse: kudzu <build|dev>`)
|
|
50
|
+
console.error(`Unknown command: ${command}\nUse: kudzu <build|dev|inspect|explain>`)
|
|
31
51
|
process.exitCode = 1
|
|
32
52
|
}
|
|
@@ -15,7 +15,7 @@ The completed `0.9.0` milestone is recorded in [`0.9-semantic-compression.md`](.
|
|
|
15
15
|
|
|
16
16
|
[`1.0-large-application-compatibility-audit.md`](./1.0-large-application-compatibility-audit.md) records the first post-0.9 probes against Memos, Apache Answer, and Actual Budget. The audit finds that reduced slices build but whole-application source retention and behavior parity do not yet pass.
|
|
17
17
|
|
|
18
|
-
[`application-capability-release-plan.md`](./application-capability-release-plan.md) is the authoritative post-0.9 execution queue, currently at `0.20.
|
|
18
|
+
[`application-capability-release-plan.md`](./application-capability-release-plan.md) is the authoritative post-0.9 execution queue, currently at `0.20.3`. It assigns one application-capability section to each minor release and one independently accepted evidence packet to each patch release from `0.10.0` through the `1.0.0` gate. It supersedes the provisional 0.10/0.11/0.12 tool-first ordering in the completed 0.9 handoff without rewriting that historical record.
|
|
19
19
|
|
|
20
20
|
## Required Invariants
|
|
21
21
|
|
|
@@ -1826,6 +1826,27 @@ artifact accounting, and `git diff --check` pass. The packet ships as
|
|
|
1826
1826
|
- **Done condition:** the first blocker can be selected without reading hundreds
|
|
1827
1827
|
of source or dependency files.
|
|
1828
1828
|
|
|
1829
|
+
Complete. `kudzu inspect --json` now runs the authoritative build and projects
|
|
1830
|
+
the existing reachable source, compatibility, ComponentAnalysis, ModuleIR,
|
|
1831
|
+
CapabilityIR, and route-artifact records into one versioned report. Modules,
|
|
1832
|
+
routes, packages, compatibility sites, capability families, semantic owners,
|
|
1833
|
+
and blockers sort before fixed section limits; totals and omitted counts keep
|
|
1834
|
+
large-project context explicit. Unreachable source, generated code, HTML, raw
|
|
1835
|
+
IR, captures, state values, and full artifact closures remain excluded.
|
|
1836
|
+
|
|
1837
|
+
One real CLI fixture proves reachable filtering, static and interactive route
|
|
1838
|
+
facts, native package classification, state ownership, project-relative paths,
|
|
1839
|
+
deterministic output, and structured blocked inventory. A synthetic 101-module
|
|
1840
|
+
and 51-blocker check proves sort-before-truncation and first-blocker retention.
|
|
1841
|
+
The implementation is one 154-line projection over existing records plus the
|
|
1842
|
+
existing build/CLI seam; it adds no analyzer, semantic primitive, compiler pass,
|
|
1843
|
+
normalization entry, runtime concept, or browser module. Representative deploy
|
|
1844
|
+
manifests and hashes remain unchanged. Worker and window graphs remain 907 raw /
|
|
1845
|
+
477 gzip and 14,456 raw / 6,160 gzip bytes; seven clean builds record a 648.7 ms
|
|
1846
|
+
median without a timing comparison. Browser-disabled and required-Chrome suites
|
|
1847
|
+
pass 295/295 tests, package smoke and artifact accounting pass, and the packet
|
|
1848
|
+
ships as `@kudzujs/core@0.16.12` with `create-kudzu@0.1.137`.
|
|
1849
|
+
|
|
1829
1850
|
### `0.20.2`: `kudzu explain --route`
|
|
1830
1851
|
|
|
1831
1852
|
- **Purpose:** trace one authored route to its selected browser artifacts.
|
|
@@ -1835,6 +1856,23 @@ artifact accounting, and `git diff --check` pass. The packet ships as
|
|
|
1835
1856
|
- **Done condition:** existing route records and artifact reports answer the
|
|
1836
1857
|
query with bounded output.
|
|
1837
1858
|
|
|
1859
|
+
Completed by `@kudzujs/core@0.16.13` and `create-kudzu@0.1.138`. The
|
|
1860
|
+
`worker-effects` fixture proves exact configured-base lookup for an effect-owned
|
|
1861
|
+
Worker route, authored source closure, `Dashboard` ownership, capability family,
|
|
1862
|
+
runtime/handler/Worker byte reasons, raw/gzip totals, hashes, deterministic
|
|
1863
|
+
output, structured missing-route diagnostics, and a static sibling with an empty
|
|
1864
|
+
JavaScript closure. The implementation is one 137-line bounded projection over
|
|
1865
|
+
existing source results, compatibility sites, route records, and the sole route
|
|
1866
|
+
artifact report plus the existing build/CLI seam. It adds no analyzer, semantic
|
|
1867
|
+
primitive, compiler pass, normalization entry, runtime concept, or browser
|
|
1868
|
+
module. Representative deploy manifests and hashes remain unchanged. Worker and
|
|
1869
|
+
window graphs remain 907 raw / 477 gzip and 14,456 raw / 6,160 gzip bytes; seven
|
|
1870
|
+
clean builds record a 605 ms median without a timing comparison. Browser-disabled
|
|
1871
|
+
and required-Chrome suites pass 296/296 tests, and package smoke and artifact
|
|
1872
|
+
accounting pass. Fixed 100-record section limits expose totals and omitted counts;
|
|
1873
|
+
the command requires one exact emitted route and intentionally excludes raw IR,
|
|
1874
|
+
HTML, generated code, captures, and state values.
|
|
1875
|
+
|
|
1838
1876
|
### `0.20.3`: Deterministic Normalize And Fix
|
|
1839
1877
|
|
|
1840
1878
|
- **Purpose:** automate only edits proven safe and useful by recorded AI trials.
|
|
@@ -2051,3 +2089,5 @@ release transaction where possible or document and publish a forward-fix patch.
|
|
|
2051
2089
|
| `0.19.4` | Closed by existing effect ownership | Preserve reaction mutation, version deduplication, stale-socket rejection, reconnect, keyed identity, exact route cleanup, fresh ownership, bounded handles/listeners/timers, and public zero-JavaScript output. | No production change; no release consumed |
|
|
2052
2090
|
| `0.19.5` | Deferred by stop condition | Resume only from a complete pinned Actual Budget workspace with an executable dependency graph and honest retention denominator. | Sparse acquisition omits required workspace and core packages; no compatibility claim |
|
|
2053
2091
|
| `0.20.0` | Released as `0.16.11` | Preserve stable diagnostic schema/codes, authored ranges, human errors, compatibility reuse, and zero browser/output delta. | Patch release retained the `0.16.x` public version line |
|
|
2092
|
+
| `0.20.1` | Released as `0.16.12` | Preserve bounded deterministic reachable inventory, first-blocker retention, existing-record reuse, and zero browser/output delta. | Patch release retained the `0.16.x` public version line |
|
|
2093
|
+
| `0.20.2` | Released as `0.16.13` | Preserve exact emitted-route lookup, authored provenance, semantic ownership, capability/artifact byte reasons and hashes, bounded deterministic output, structured missing-route diagnostics, and zero-JavaScript explanation. | Patch release retained the `0.16.x` public version line |
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# Current Compiler Architecture
|
|
2
2
|
|
|
3
|
-
This maps the current `0.16.
|
|
3
|
+
This maps the current `0.16.13` architecture, built on the completed `0.9.0` semantic-compression release and `0.8.23` Goal A compiler foundation. The active application packet is `0.20.3`; file and function names are the stable references, while line numbers are intentionally omitted because later work may still move code.
|
|
4
4
|
|
|
5
5
|
## Responsibility Map
|
|
6
6
|
|
|
7
7
|
| Responsibility | Current owner | Current contract |
|
|
8
8
|
|---|---|---|
|
|
9
|
-
| CLI entry | [`bin/kudzu.mjs`](../../bin/kudzu.mjs) | Dispatches build and
|
|
9
|
+
| CLI entry | [`bin/kudzu.mjs`](../../bin/kudzu.mjs) | Dispatches build, development, inspection, and exact-route explanation commands. |
|
|
10
10
|
| Project session | [`framework/compiler/project-session.mjs`](../../framework/compiler/project-session.mjs), `createProjectSession()` | Owns one absolute root, standard project paths, source records, bound graph operations, and Worker compiler. Production builds use one session; development retains it across rebuilds. Omitted roots resolve from call-time CWD. |
|
|
11
11
|
| Parsed module cache and symbols | [`framework/compiler/project-session.mjs`](../../framework/compiler/project-session.mjs) | Parses each unchanged source module once per ProjectSession and records source-local declaration/import/re-export sites. Stable ModuleSymbol records resolve direct, aliased, barrel, and `export *` exports with cycle and ambiguity checks; repeated resolutions are cached against their source dependencies, and normalization consumers locate the resolved SiteId in a fresh clone with independent parent links. |
|
|
12
12
|
| Build orchestration | [`framework/build.mjs`](../../framework/build.mjs), `build()`, `buildWithSession()` | Coordinates config, discovery, source compilation, RouteBuildRecord collection, CapabilityIR projection, generator invocation, artifact emission, and `afterBuild`. A retained session caches source results and pre-family route renders by page graph; successful builds alone replace that cache. |
|
|
@@ -22,6 +22,7 @@ This maps the current `0.16.11` architecture, built on the completed `0.9.0` sem
|
|
|
22
22
|
| Component ownership analysis | [`framework/compiler/analysis/component-analysis.mjs`](../../framework/compiler/analysis/component-analysis.mjs) | Produces ComponentAnalysis v2 with ordered JSON-safe owner and specialization slots for state, setters, props, refs, IDs, direct SignalIR links, direct static property/consumer links, structural OwnerRefs, source-local SiteIds, and source provenance. Three direct callback/ref component boundaries specialize into the same parent signal and intrinsic ownership; Context action-private setters may remain compiler-only and receive collision-free consumer-local aliases; AST identity remains private to its source-local session. |
|
|
23
23
|
| Per-source descriptor registration | [`framework/compiler/descriptor-session.mjs`](../../framework/compiler/descriptor-session.mjs), `createSemanticArtifact()`, `createDescriptorSession()` | Keeps AST descriptors private during analysis, then finalizes ModuleIR v2 with deterministic SymbolRef, SharedStateIR, SharedActionIR, SignalIR, HandlerIR, BindingIR, DerivedIR, EffectIR, KeyedBlockIR, and ImportIR slots. One fail-closed boundary validates every source-local and component ownership edge before build-module generation. |
|
|
24
24
|
| Route artifact graph | [`framework/compiler/route-build-record.mjs`](../../framework/compiler/route-build-record.mjs), `createRouteBuildRecord()`, `planRouteArtifacts()`; [`framework/compiler/route-artifact-report.mjs`](../../framework/compiler/route-artifact-report.mjs), `createRouteArtifactReport()` | Validates each rendered route's RouteIR, capabilities, entry paths, styles, and exact handler/effect references. Handler and Worker esbuild metafiles project transitive output edges back through those records into deterministic per-route capability signatures and runtime requirements plus emitted handler, Worker, stylesheet, and shared-chunk closure. |
|
|
25
|
+
| Tool projections | [`framework/compiler/inspection-report.mjs`](../../framework/compiler/inspection-report.mjs), `createInspectionReport()`; [`framework/compiler/explanation-report.mjs`](../../framework/compiler/explanation-report.mjs), `createExplanationReport()` | Projects bounded deterministic reachable-application inventory and one exact route's authored provenance, ownership, capability, artifact byte reasons, hashes, and zero-JavaScript explanation from existing build records. Raw semantic IR, generated code, HTML, captures, and state values remain private. |
|
|
25
26
|
| Route contract validation | [`framework/compiler/route-ir.mjs`](../../framework/compiler/route-ir.mjs), `assertRouteIR()` | Fails before artifact selection for invalid state/parameter identity, commands, native/effect captures and dependencies, reactive descriptors, conditions, keyed-list identity/ownership, marker fields, or JSON safety. Immutable in-memory contracts validate once by identity. |
|
|
26
27
|
| Command IR and codegen | [`framework/compiler/optimize/command-specialization.mjs`](../../framework/compiler/optimize/command-specialization.mjs), [`framework/compiler/ir/module-ir.mjs`](../../framework/compiler/ir/module-ir.mjs), [`framework/compiler/codegen/command-codegen.mjs`](../../framework/compiler/codegen/command-codegen.mjs) | Direct commands use the existing fast path; proven immutable state aliases and one-call local helpers specialize to the same JSON-safe command ModuleIR. Recursion, escape, mutation, and dynamic helper dispatch fail explicitly, while unrelated handlers retain native ESM. Codegen emits the existing `__kBehavior` AST and command ABI. |
|
|
27
28
|
| Source compilation | [`framework/compiler/source-compiler.mjs`](../../framework/compiler/source-compiler.mjs), `compileSource()` | Runs TypeScript with the Kudzu transformer, rejects surviving React/Router references, and returns a JSON-safe project-relative build module, component analysis, ModuleIR, optional handler module, and imported assets without filesystem writes. |
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
## Status
|
|
4
4
|
|
|
5
|
-
Completed compiler-foundation record and longer-term plan after `0.8.35`. The [`0.9-semantic-compression.md`](./0.9-semantic-compression.md) execution queue is complete. The active queue for current work is [`application-capability-release-plan.md`](./application-capability-release-plan.md) at packet `0.20.
|
|
5
|
+
Completed compiler-foundation record and longer-term plan after `0.8.35`. The [`0.9-semantic-compression.md`](./0.9-semantic-compression.md) execution queue is complete. The active queue for current work is [`application-capability-release-plan.md`](./application-capability-release-plan.md) at packet `0.20.3`. This document does not mark any remaining planned capability as supported and does not authorize a React runtime, VDOM, hydration, retained browser component tree, generic rerenderer, public store/query/resource API, SPA router, or islands.
|
|
6
6
|
|
|
7
|
-
[`MIGRATION_ROADMAP.md`](../../MIGRATION_ROADMAP.md) remains the product authority for product invariants and fixture-driven feature selection. [`application-capability-release-plan.md`](./application-capability-release-plan.md) is authoritative for current work order and evidence at packet `0.20.
|
|
7
|
+
[`MIGRATION_ROADMAP.md`](../../MIGRATION_ROADMAP.md) remains the product authority for product invariants and fixture-driven feature selection. [`application-capability-release-plan.md`](./application-capability-release-plan.md) is authoritative for current work order and evidence at packet `0.20.3`; this plan retains the completed foundation, deferred program, and long-term production gates. If implementation evidence changes either boundary, update the relevant document before broadening a patch.
|
|
8
8
|
|
|
9
9
|
## Product Outcome
|
|
10
10
|
|
|
@@ -483,4 +483,4 @@ The first comparison is Kudzu versus React + Vite using the same agent, model, t
|
|
|
483
483
|
|
|
484
484
|
## Immediate Decision
|
|
485
485
|
|
|
486
|
-
All listed foundation and `0.9` slices are complete. Current work continues at `0.20.
|
|
486
|
+
All listed foundation and `0.9` slices are complete. Current work continues at `0.20.3` deterministic normalize and fix under the [`application-capability-release-plan.md`](./application-capability-release-plan.md); keep ResourceIR limited to qualifying independent fixtures, and do not add range ownership, virtualization, optimistic transactions, a public adapter/store API, or a router before evidence justifies them.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Planned Version Sequence
|
|
2
2
|
|
|
3
|
-
This is an execution sequence, not release history. `0.8.16` through `0.8.62` are completed scopes represented by package/release records. The `0.10.0` through `1.0.0` minor/patch sequence is maintained in [`application-capability-release-plan.md`](./application-capability-release-plan.md), currently at `0.20.
|
|
3
|
+
This is an execution sequence, not release history. `0.8.16` through `0.8.62` are completed scopes represented by package/release records. The `0.10.0` through `1.0.0` minor/patch sequence is maintained in [`application-capability-release-plan.md`](./application-capability-release-plan.md), currently at `0.20.3`; that plan supersedes the provisional tool-first 0.10/0.11/0.12 ordering in the completed 0.9 handoff.
|
|
4
4
|
|
|
5
5
|
Keep each patch behavior-preserving and independently reviewable. If a boundary proves inseparable, revise this plan before combining releases; do not silently broaden a patch.
|
|
6
6
|
|
|
@@ -66,7 +66,7 @@ Keep each patch behavior-preserving and independently reviewable. If a boundary
|
|
|
66
66
|
|
|
67
67
|
## Generator Versions
|
|
68
68
|
|
|
69
|
-
`create-kudzu@0.1.
|
|
69
|
+
`create-kudzu@0.1.138` retains the explicit install instructions and generates projects with `@kudzujs/core@^0.16.13`.
|
|
70
70
|
|
|
71
71
|
## Release Boundary
|
|
72
72
|
|
package/framework/build.mjs
CHANGED
|
@@ -5,7 +5,9 @@ import { pathToFileURL } from "node:url"
|
|
|
5
5
|
import { build as bundle, transform } from "esbuild"
|
|
6
6
|
import { createEffectCodegen } from "./compiler/effect-codegen.mjs"
|
|
7
7
|
import { createCompatibilityReport } from "./compiler/compatibility-registry.mjs"
|
|
8
|
-
import { normalizeDiagnosticError } from "./compiler/diagnostics.mjs"
|
|
8
|
+
import { createDiagnosticError, diagnosticEnvelope, normalizeDiagnosticError } from "./compiler/diagnostics.mjs"
|
|
9
|
+
import { createExplanationReport } from "./compiler/explanation-report.mjs"
|
|
10
|
+
import { createInspectionReport } from "./compiler/inspection-report.mjs"
|
|
9
11
|
import { generateListRuntime } from "./compiler/list-runtime-codegen.mjs"
|
|
10
12
|
import { assetPath, browserPath, relativeModulePath, withBase } from "./compiler/path-helpers.mjs"
|
|
11
13
|
import { createProjectSession } from "./compiler/project-session.mjs"
|
|
@@ -38,7 +40,17 @@ export async function build({ quiet = false, minify = true, root: projectRoot =
|
|
|
38
40
|
return buildWithSession(project, { quiet, minify, retainCache: false })
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
export async function
|
|
43
|
+
export async function inspect({ minify = true, root: projectRoot = process.cwd() } = {}) {
|
|
44
|
+
const project = createProjectSession(projectRoot)
|
|
45
|
+
return buildWithSession(project, { quiet: true, minify, retainCache: false, inspection: true })
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function explain({ route, minify = true, root: projectRoot = process.cwd() } = {}) {
|
|
49
|
+
const project = createProjectSession(projectRoot)
|
|
50
|
+
return buildWithSession(project, { quiet: true, minify, retainCache: false, explanationRoute: route })
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function buildWithSession(project, { changedFiles, quiet = false, minify = true, retainCache = true, inspection = false, explanationRoute } = {}) {
|
|
42
54
|
const { root, outputDirectory } = project
|
|
43
55
|
const stagedOutput = join(root, ".kudzu-dist-staging")
|
|
44
56
|
const backupOutput = join(root, ".kudzu-dist-backup")
|
|
@@ -47,13 +59,16 @@ export async function buildWithSession(project, { changedFiles, quiet = false, m
|
|
|
47
59
|
try {
|
|
48
60
|
await recoverOutput(outputDirectory, backupOutput)
|
|
49
61
|
await rm(stagedOutput, { recursive: true, force: true })
|
|
50
|
-
const { result, pageCount, behaviorCount, cache } = await buildInto(project, stagedOutput, { changedFiles, minify, quiet, retainCache })
|
|
62
|
+
const { result, pageCount, behaviorCount, cache, inspectionData, explanation } = await buildInto(project, stagedOutput, { changedFiles, minify, quiet, retainCache, explanationRoute })
|
|
51
63
|
await promoteOutput(stagedOutput, outputDirectory, backupOutput)
|
|
52
64
|
project.buildCache = retainCache ? cache : undefined
|
|
53
65
|
if (!quiet) console.log(`Built ${pageCount} page(s), ${behaviorCount} interactive page(s) into dist/`)
|
|
54
|
-
return result
|
|
66
|
+
return explanationRoute ? explanation : inspection ? createInspectionReport(inspectionData) : result
|
|
55
67
|
} catch (error) {
|
|
56
|
-
|
|
68
|
+
const normalized = normalizeDiagnosticError(error, root)
|
|
69
|
+
const envelope = inspection && diagnosticEnvelope(normalized)
|
|
70
|
+
if (envelope) return createInspectionReport({ diagnostics: envelope.diagnostics })
|
|
71
|
+
throw normalized
|
|
57
72
|
} finally {
|
|
58
73
|
try {
|
|
59
74
|
await rm(stagedOutput, { recursive: true, force: true })
|
|
@@ -67,7 +82,7 @@ export async function buildWithSession(project, { changedFiles, quiet = false, m
|
|
|
67
82
|
}
|
|
68
83
|
}
|
|
69
84
|
|
|
70
|
-
async function buildInto(project, outputDirectory, { changedFiles, minify, quiet, retainCache }) {
|
|
85
|
+
async function buildInto(project, outputDirectory, { changedFiles, minify, quiet, retainCache, explanationRoute }) {
|
|
71
86
|
const { root, sourceDirectory, pagesDirectory, workDirectory } = project
|
|
72
87
|
const previous = retainCache ? project.buildCache : undefined
|
|
73
88
|
project.buildGeneration = (project.buildGeneration ?? 0) + 1
|
|
@@ -107,7 +122,8 @@ async function buildInto(project, outputDirectory, { changedFiles, minify, quiet
|
|
|
107
122
|
if (!pageFiles.length) throw new Error("No pages found in src/pages/")
|
|
108
123
|
const pageSources = new Map(pageFiles.map(file => [file, new Set(reachableSourceFiles([file], allSourceFileSet, sourceIndex))]))
|
|
109
124
|
const sourceFiles = [...new Set([...pageSources.values()].flatMap(files => [...files]))].sort()
|
|
110
|
-
|
|
125
|
+
const compatibility = createCompatibilityReport(sourceFiles.map(file => ({ file: relative(root, file).replaceAll(sep, "/"), source: sourceIndex.get(file) })))
|
|
126
|
+
await writePrettyJson(join(workDirectory, "kudzu-compatibility.json"), compatibility)
|
|
111
127
|
const sourceFileSet = project.sourceFiles
|
|
112
128
|
sourceFileSet.clear()
|
|
113
129
|
for (const file of sourceFiles) sourceFileSet.add(file)
|
|
@@ -174,6 +190,7 @@ async function buildInto(project, outputDirectory, { changedFiles, minify, quiet
|
|
|
174
190
|
const bindingPlaceholder = placeholders.binding
|
|
175
191
|
const listPlaceholder = placeholders.list
|
|
176
192
|
const pageRenders = new Map()
|
|
193
|
+
const routePageFiles = new Map()
|
|
177
194
|
let renderedPages = 0
|
|
178
195
|
|
|
179
196
|
for (const pageFile of pageFiles) {
|
|
@@ -294,6 +311,7 @@ async function buildInto(project, outputDirectory, { changedFiles, minify, quiet
|
|
|
294
311
|
runtimeSchema
|
|
295
312
|
})
|
|
296
313
|
routeRecords.push(record)
|
|
314
|
+
routePageFiles.set(record, pageFile)
|
|
297
315
|
if (navigationGroup) navigationGroup.buildRecords.push(record)
|
|
298
316
|
routeDrafts.push({ record, runtimeSchema, navigationGroup, applicationRoute, effectPath, nativePath, paramPath })
|
|
299
317
|
if (!retainCache && config.afterBuild === undefined) {
|
|
@@ -524,9 +542,21 @@ async function buildInto(project, outputDirectory, { changedFiles, minify, quiet
|
|
|
524
542
|
await config.afterBuild({ root, outDir: outputDirectory, sourceDir: sourceDirectory, base, routes: plans.map(plan => plan.route), plans, rewrites: sortedRewrites, artifacts })
|
|
525
543
|
}
|
|
526
544
|
|
|
545
|
+
let explanation
|
|
546
|
+
if (explanationRoute) {
|
|
547
|
+
const artifact = artifacts.routes.find(entry => entry.route === explanationRoute)
|
|
548
|
+
const record = routeRecords.find(entry => entry.route === explanationRoute)
|
|
549
|
+
if (!artifact || !record) throw createDiagnosticError({ code: "explain.route.not-found", stage: "explain", message: `No emitted route exactly matches ${JSON.stringify(explanationRoute)}.`, suggestion: "Use an exact emitted route listed by kudzu inspect --json." })
|
|
550
|
+
const pageFile = routePageFiles.get(record)
|
|
551
|
+
const sourceFiles = [...pageSources.get(pageFile)].map(file => relative(root, file).replaceAll(sep, "/"))
|
|
552
|
+
explanation = await createExplanationReport({ route: explanationRoute, record, artifact, entrySource: relative(root, pageFile).replaceAll(sep, "/"), sourceFiles, sourceResults, compatibility, outputDirectory, base })
|
|
553
|
+
}
|
|
554
|
+
|
|
527
555
|
const incremental = { compiledModules, renderedPages }
|
|
528
556
|
return {
|
|
529
557
|
result: { sourceResults, incremental },
|
|
558
|
+
inspectionData: { sourceFiles: sourceFiles.map(file => relative(root, file).replaceAll(sep, "/")), sourceResults, compatibility, artifacts },
|
|
559
|
+
explanation,
|
|
530
560
|
pageCount: routeRecords.length,
|
|
531
561
|
behaviorCount,
|
|
532
562
|
cache: { pageRenders, pageSources, placeholders, sourceResults: sourceResultsByFile }
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { createHash } from "node:crypto"
|
|
2
|
+
import { readFile } from "node:fs/promises"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
import { gzipSync } from "node:zlib"
|
|
5
|
+
|
|
6
|
+
const limits = Object.freeze({ sources: 100, normalization: 100, owners: 100, effects: 100, artifacts: 100 })
|
|
7
|
+
|
|
8
|
+
export async function createExplanationReport({ route, record, artifact, entrySource, sourceFiles, sourceResults, compatibility, outputDirectory, base }) {
|
|
9
|
+
const sources = [...sourceFiles].sort(compareText)
|
|
10
|
+
const sourceSet = new Set(sources)
|
|
11
|
+
const results = sourceResults.filter(result => sourceSet.has(result.file))
|
|
12
|
+
const normalization = compatibility.sites.filter(site => sourceSet.has(site.file)).map(site => ({ ...site, provenance: "reachable-source" }))
|
|
13
|
+
const owners = results.flatMap(result => [
|
|
14
|
+
...result.componentAnalysis.owners.map(owner => ownerFact(result.file, "component", owner)),
|
|
15
|
+
...result.componentAnalysis.specializations.map(owner => ownerFact(result.file, "specialization", owner)),
|
|
16
|
+
]).sort((left, right) => compareText(left.module, right.module) || compareText(left.kind, right.kind) || left.slot - right.slot)
|
|
17
|
+
const effects = effectFacts(record, results, sources)
|
|
18
|
+
const artifacts = await artifactFacts(artifact, outputDirectory, base)
|
|
19
|
+
const eager = artifacts.filter(entry => !entry.reasons.includes("lazy-handler-chunk") && !entry.external)
|
|
20
|
+
const lazy = artifacts.filter(entry => entry.reasons.includes("lazy-handler-chunk") && !entry.external)
|
|
21
|
+
const javascript = artifacts.filter(entry => entry.path.endsWith(".js") && !entry.external)
|
|
22
|
+
const zeroJavaScript = artifact.runtime.family === null && javascript.length === 0
|
|
23
|
+
const sections = { sources, normalization, owners, effects, artifacts }
|
|
24
|
+
return {
|
|
25
|
+
version: 1,
|
|
26
|
+
status: "ready",
|
|
27
|
+
route,
|
|
28
|
+
entrySource,
|
|
29
|
+
runtimeRoute: Boolean(record.runtimeSchema),
|
|
30
|
+
capability: { signature: artifact.capability.signature, family: artifact.runtime.family, facts: enabledFacts(artifact.capability.manifest) },
|
|
31
|
+
...Object.fromEntries(Object.entries(sections).map(([name, entries]) => [name, entries.slice(0, limits[name])])),
|
|
32
|
+
bytes: {
|
|
33
|
+
eagerRawBytes: sum(eager, "rawBytes"),
|
|
34
|
+
eagerGzipBytes: sum(eager, "gzipBytes"),
|
|
35
|
+
lazyRawBytes: sum(lazy, "rawBytes"),
|
|
36
|
+
lazyGzipBytes: sum(lazy, "gzipBytes"),
|
|
37
|
+
javascriptRawBytes: sum(javascript, "rawBytes"),
|
|
38
|
+
javascriptGzipBytes: sum(javascript, "gzipBytes"),
|
|
39
|
+
},
|
|
40
|
+
zeroJavaScript: {
|
|
41
|
+
value: zeroJavaScript,
|
|
42
|
+
scope: "compiler-selected-capability-artifacts",
|
|
43
|
+
reasons: zeroJavaScript
|
|
44
|
+
? ["no behavior runtime family", "no runtime or handler entry", "no Worker graph"]
|
|
45
|
+
: ["route selects one or more browser capability artifacts"],
|
|
46
|
+
},
|
|
47
|
+
limits,
|
|
48
|
+
summary: Object.fromEntries(Object.entries(sections).map(([name, entries]) => [name, entries.length])),
|
|
49
|
+
omitted: Object.fromEntries(Object.entries(sections).map(([name, entries]) => [name, Math.max(0, entries.length - limits[name])])),
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function effectFacts(record, results, sources) {
|
|
54
|
+
const facts = []
|
|
55
|
+
for (const reference of record.artifacts.effects) {
|
|
56
|
+
const result = results.find(entry => entry.handlerModule && reference.module.endsWith(`/assets/${entry.handlerModule.path}`))
|
|
57
|
+
if (!result) continue
|
|
58
|
+
const handler = result.moduleIR.handlers.find(entry => entry.exportName === reference.handler)
|
|
59
|
+
const effect = result.moduleIR.effects.find(entry => entry.setup?.handler === handler?.slot)
|
|
60
|
+
if (!handler || !effect) continue
|
|
61
|
+
const owner = effect.ownership?.owner
|
|
62
|
+
const descriptor = owner?.kind === "component" ? result.componentAnalysis.owners[owner.slot] : owner?.kind === "specialization" ? result.componentAnalysis.specializations[owner.slot] : undefined
|
|
63
|
+
facts.push({
|
|
64
|
+
module: result.file,
|
|
65
|
+
handler: reference.handler,
|
|
66
|
+
...(handler.source ? { source: handler.source } : {}),
|
|
67
|
+
owner: descriptor?.name ?? descriptor?.component?.name ?? owner?.kind ?? "module",
|
|
68
|
+
cleanup: Boolean(effect.cleanup),
|
|
69
|
+
dependencies: effect.dependencies?.length ?? 0,
|
|
70
|
+
workers: effect.workers.map(worker => {
|
|
71
|
+
const matches = sources.filter(file => file === worker.root || file.endsWith(`/${worker.root}`))
|
|
72
|
+
return matches.length === 1 ? matches[0] : worker.root
|
|
73
|
+
}).sort(compareText),
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
return facts.sort((left, right) => compareText(left.module, right.module) || compareText(left.handler, right.handler))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function artifactFacts(route, outputDirectory, base) {
|
|
80
|
+
const edges = [
|
|
81
|
+
...route.runtime.entries.map(path => [path, "runtime-entry"]),
|
|
82
|
+
...route.runtime.requirements.map(path => [path, "runtime-requirement"]),
|
|
83
|
+
...route.handlers.entries.map(path => [path, "handler-entry"]),
|
|
84
|
+
...route.handlers.chunks.map(path => [path, "handler-chunk"]),
|
|
85
|
+
...route.handlers.lazyChunks.map(path => [path, "lazy-handler-chunk"]),
|
|
86
|
+
...route.workers.flatMap(worker => [[worker.entry, `worker:${worker.source}`], ...worker.chunks.map(path => [path, `worker-chunk:${worker.source}`])]),
|
|
87
|
+
...route.styles.map(path => [path, "style"]),
|
|
88
|
+
]
|
|
89
|
+
const selected = new Map()
|
|
90
|
+
for (const [url, reason] of edges) {
|
|
91
|
+
const reasons = selected.get(url) ?? new Set()
|
|
92
|
+
reasons.add(reason)
|
|
93
|
+
selected.set(url, reasons)
|
|
94
|
+
}
|
|
95
|
+
return Promise.all([...selected].sort(([left], [right]) => compareText(left, right)).map(async ([url, reasons]) => {
|
|
96
|
+
if (/^https?:\/\//.test(url)) return { path: url, reasons: [...reasons].sort(compareText), external: true }
|
|
97
|
+
const pathname = new URL(url, "https://kudzu.invalid").pathname
|
|
98
|
+
const deployed = base && pathname.startsWith(`${base}/`) ? pathname.slice(base.length + 1) : pathname.slice(1)
|
|
99
|
+
const contents = await readFile(join(outputDirectory, deployed))
|
|
100
|
+
return {
|
|
101
|
+
path: deployed,
|
|
102
|
+
reasons: [...reasons].sort(compareText),
|
|
103
|
+
rawBytes: contents.byteLength,
|
|
104
|
+
gzipBytes: gzipSync(contents).byteLength,
|
|
105
|
+
sha256: createHash("sha256").update(contents).digest("hex"),
|
|
106
|
+
}
|
|
107
|
+
}))
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function ownerFact(module, kind, owner) {
|
|
111
|
+
return {
|
|
112
|
+
module,
|
|
113
|
+
kind,
|
|
114
|
+
slot: owner.slot,
|
|
115
|
+
name: owner.name ?? owner.component?.name ?? "anonymous",
|
|
116
|
+
states: owner.states?.length ?? 0,
|
|
117
|
+
refs: owner.refs?.length ?? 0,
|
|
118
|
+
ids: owner.ids?.length ?? 0,
|
|
119
|
+
provenance: "reachable-source",
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function enabledFacts(value, prefix = "") {
|
|
124
|
+
const facts = []
|
|
125
|
+
for (const [key, entry] of Object.entries(value ?? {}).sort(([left], [right]) => compareText(left, right))) {
|
|
126
|
+
if (key === "version") continue
|
|
127
|
+
const path = prefix ? `${prefix}.${key}` : key
|
|
128
|
+
if (entry === true) facts.push(path)
|
|
129
|
+
else if (typeof entry === "number" && entry > 0) facts.push(`${path}=${entry}`)
|
|
130
|
+
else if (Array.isArray(entry)) for (const item of entry) facts.push(`${path}=${item}`)
|
|
131
|
+
else if (entry && typeof entry === "object") facts.push(...enabledFacts(entry, path))
|
|
132
|
+
}
|
|
133
|
+
return facts
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const sum = (entries, field) => entries.reduce((total, entry) => total + entry[field], 0)
|
|
137
|
+
const compareText = (left, right) => left < right ? -1 : left > right ? 1 : 0
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
const limits = Object.freeze({ modules: 100, routes: 100, packages: 100, compatibilitySites: 100, capabilities: 50, owners: 100, blockers: 50 })
|
|
2
|
+
|
|
3
|
+
export function createInspectionReport({ sourceFiles = [], sourceResults = [], compatibility, artifacts, diagnostics = [] } = {}) {
|
|
4
|
+
const complete = diagnostics.length === 0
|
|
5
|
+
const modules = complete ? moduleFacts(sourceFiles, sourceResults) : []
|
|
6
|
+
const routes = complete ? routeFacts(artifacts?.routes ?? []) : []
|
|
7
|
+
const packages = complete ? [...(compatibility?.packages ?? [])] : []
|
|
8
|
+
const compatibilitySites = complete ? [...(compatibility?.sites ?? [])] : []
|
|
9
|
+
const capabilities = complete ? capabilityFacts(artifacts?.runtimeFamilies ?? []) : []
|
|
10
|
+
const owners = complete ? ownerFacts(sourceResults) : []
|
|
11
|
+
const blockers = complete ? compatibilityBlockers(compatibilitySites) : diagnosticBlockers(diagnostics)
|
|
12
|
+
const sections = { modules, routes, packages, compatibilitySites, capabilities, owners, blockers }
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
version: 1,
|
|
16
|
+
status: blockers.some(blocker => blocker.severity === "error") ? "blocked" : "ready",
|
|
17
|
+
inventoryComplete: complete,
|
|
18
|
+
limits,
|
|
19
|
+
summary: Object.fromEntries(Object.entries(sections).map(([name, entries]) => [name, entries.length])),
|
|
20
|
+
...Object.fromEntries(Object.entries(sections).map(([name, entries]) => [name, entries.slice(0, limits[name])])),
|
|
21
|
+
omitted: Object.fromEntries(Object.entries(sections).map(([name, entries]) => [name, Math.max(0, entries.length - limits[name])])),
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function moduleFacts(sourceFiles, sourceResults) {
|
|
26
|
+
const results = new Map(sourceResults.map(result => [result.file, result]))
|
|
27
|
+
return [...sourceFiles].sort(compareText).map(file => {
|
|
28
|
+
const result = results.get(file)
|
|
29
|
+
const moduleIR = result?.moduleIR
|
|
30
|
+
return compact({
|
|
31
|
+
file,
|
|
32
|
+
kind: file.endsWith(".worker.ts") ? "worker" : file.startsWith("src/pages/") && file.endsWith(".tsx") ? "page" : "module",
|
|
33
|
+
owners: result?.componentAnalysis.owners.length ?? 0,
|
|
34
|
+
specializations: result?.componentAnalysis.specializations.length ?? 0,
|
|
35
|
+
signals: moduleIR?.signals.length ?? 0,
|
|
36
|
+
sharedStates: moduleIR?.sharedStates.length ?? 0,
|
|
37
|
+
sharedActions: moduleIR?.sharedActions.length ?? 0,
|
|
38
|
+
handlers: moduleIR?.handlers.length ?? 0,
|
|
39
|
+
bindings: moduleIR?.bindings.length ?? 0,
|
|
40
|
+
derived: moduleIR?.derived.length ?? 0,
|
|
41
|
+
effects: moduleIR?.effects.length ?? 0,
|
|
42
|
+
keyedBlocks: moduleIR?.keyedBlocks.length ?? 0,
|
|
43
|
+
imports: moduleIR?.imports.length ?? 0,
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function routeFacts(routes) {
|
|
49
|
+
return [...routes].sort((left, right) => compareText(left.route, right.route)).map(route => ({
|
|
50
|
+
route: route.route,
|
|
51
|
+
runtimeFamily: route.runtime.family,
|
|
52
|
+
static: route.runtime.family === null && route.runtime.entries.length === 0 && route.handlers.entries.length === 0 && route.handlers.chunks.length === 0 && route.handlers.lazyChunks.length === 0 && route.workers.length === 0,
|
|
53
|
+
capabilities: enabledFacts(route.capability.manifest),
|
|
54
|
+
artifacts: {
|
|
55
|
+
runtimeEntries: route.runtime.entries.length,
|
|
56
|
+
runtimeRequirements: route.runtime.requirements.length,
|
|
57
|
+
handlerEntries: route.handlers.entries.length,
|
|
58
|
+
chunks: route.handlers.chunks.length,
|
|
59
|
+
lazyChunks: route.handlers.lazyChunks.length,
|
|
60
|
+
workers: route.workers.length,
|
|
61
|
+
styles: route.styles.length,
|
|
62
|
+
},
|
|
63
|
+
}))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function capabilityFacts(families) {
|
|
67
|
+
return [...families].sort((left, right) => compareText(left.id, right.id)).map(family => ({
|
|
68
|
+
id: family.id,
|
|
69
|
+
navigation: family.navigation,
|
|
70
|
+
routes: family.routes.length,
|
|
71
|
+
facts: enabledFacts(family.manifest),
|
|
72
|
+
requirements: family.requirements.length,
|
|
73
|
+
}))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function ownerFacts(sourceResults) {
|
|
77
|
+
const owners = []
|
|
78
|
+
for (const result of sourceResults) {
|
|
79
|
+
const effects = result.moduleIR.effects
|
|
80
|
+
const blocks = result.moduleIR.keyedBlocks
|
|
81
|
+
for (const owner of result.componentAnalysis.owners) owners.push(ownerFact(result.file, "component", owner, effects, blocks))
|
|
82
|
+
for (const owner of result.componentAnalysis.specializations) owners.push(ownerFact(result.file, "specialization", owner, effects, blocks))
|
|
83
|
+
}
|
|
84
|
+
return owners.sort((left, right) => compareText(left.module, right.module) || compareText(left.kind, right.kind) || left.slot - right.slot)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function ownerFact(module, kind, owner, effects, blocks) {
|
|
88
|
+
return {
|
|
89
|
+
module,
|
|
90
|
+
kind,
|
|
91
|
+
slot: owner.slot,
|
|
92
|
+
name: typeof owner.name === "string" ? owner.name : typeof owner.component === "string" ? owner.component : owner.component?.name ?? "anonymous",
|
|
93
|
+
...compact({
|
|
94
|
+
stateCount: owner.states?.length ?? 0,
|
|
95
|
+
refCount: owner.refs?.length ?? 0,
|
|
96
|
+
idCount: owner.ids?.length ?? 0,
|
|
97
|
+
effectCount: effects.filter(effect => effect.ownership?.owner?.kind === kind && effect.ownership.owner.slot === owner.slot).length,
|
|
98
|
+
keyedBlockCount: kind === "specialization" ? blocks.filter(block => block.specializations?.includes(owner.slot)).length : 0,
|
|
99
|
+
}),
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function compatibilityBlockers(sites) {
|
|
104
|
+
return sites.filter(site => site.classification === "Unsupported" || site.classification === "Partial").map(site => ({
|
|
105
|
+
kind: "compatibility",
|
|
106
|
+
severity: site.classification === "Unsupported" ? "error" : "review",
|
|
107
|
+
code: site.rule,
|
|
108
|
+
file: site.file,
|
|
109
|
+
location: site.location,
|
|
110
|
+
package: site.package,
|
|
111
|
+
imported: site.imported,
|
|
112
|
+
classification: site.classification,
|
|
113
|
+
})).sort(compareBlockers)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function diagnosticBlockers(diagnostics) {
|
|
117
|
+
return diagnostics.map(diagnostic => ({
|
|
118
|
+
kind: "diagnostic",
|
|
119
|
+
severity: diagnostic.severity,
|
|
120
|
+
code: diagnostic.code,
|
|
121
|
+
stage: diagnostic.stage,
|
|
122
|
+
message: diagnostic.message,
|
|
123
|
+
...(diagnostic.source ? { source: diagnostic.source } : {}),
|
|
124
|
+
compatibilityClass: diagnostic.compatibilityClass,
|
|
125
|
+
suggestion: diagnostic.suggestion,
|
|
126
|
+
})).sort(compareBlockers)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function enabledFacts(value, prefix = "") {
|
|
130
|
+
const facts = []
|
|
131
|
+
for (const [key, entry] of Object.entries(value ?? {}).sort(([left], [right]) => compareText(left, right))) {
|
|
132
|
+
if (key === "version") continue
|
|
133
|
+
const path = prefix ? `${prefix}.${key}` : key
|
|
134
|
+
if (entry === true) facts.push(path)
|
|
135
|
+
else if (typeof entry === "number" && entry > 0) facts.push(`${path}=${entry}`)
|
|
136
|
+
else if (Array.isArray(entry)) for (const item of entry) facts.push(`${path}=${item}`)
|
|
137
|
+
else if (entry && typeof entry === "object") facts.push(...enabledFacts(entry, path))
|
|
138
|
+
}
|
|
139
|
+
return facts
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function compact(record) {
|
|
143
|
+
return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== 0))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function compareBlockers(left, right) {
|
|
147
|
+
const leftFile = left.file ?? left.source?.file ?? ""
|
|
148
|
+
const rightFile = right.file ?? right.source?.file ?? ""
|
|
149
|
+
const leftLocation = left.location ?? left.source?.start ?? {}
|
|
150
|
+
const rightLocation = right.location ?? right.source?.start ?? {}
|
|
151
|
+
return compareText(leftFile, rightFile) || (leftLocation.line ?? 0) - (rightLocation.line ?? 0) || (leftLocation.column ?? 0) - (rightLocation.column ?? 0) || compareText(left.code, right.code)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function compareText(left, right) {
|
|
155
|
+
return left < right ? -1 : left > right ? 1 : 0
|
|
156
|
+
}
|