From 12040618b9f83ea9370345a2c908f6ffc606fc8a Mon Sep 17 00:00:00 2001 From: Rahul Krishna Date: Fri, 11 Sep 2026 07:51:23 -0400 Subject: [PATCH 1/3] =?UTF-8?q?docs(roadmap):=20add=20the=202026-09-11=20p?= =?UTF-8?q?ass=20=E2=80=94=20the=20Java=20web=20layer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Amends the roadmap in place with a second pass: view templates as artifacts with a role, view-dispatch edges, descriptor-derived routes, a template-internal code model, and template-to-code dataflow. Records the collision between a new template role and the still-open roles-vocabulary errata (#48), and folds that resolution into the same design session. Bundler-output inventory is recorded as not-now. --- docs/design/roadmap.md | 127 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 126 insertions(+), 1 deletion(-) diff --git a/docs/design/roadmap.md b/docs/design/roadmap.md index 604582b..cf8c55e 100644 --- a/docs/design/roadmap.md +++ b/docs/design/roadmap.md @@ -1,6 +1,6 @@ # CLDK roadmap -**Pass:** 2026-08-07 +**Pass:** 2026-08-07 (schema v2 consistency) · amended 2026-09-11 (Java web layer, below) **Planned with:** Rahul Krishna **Status:** current (supersede by editing, not by adding a second roadmap) @@ -208,3 +208,128 @@ spec-only pass has no pull request to close. The first epic under this roadmap w whichever of candidates 7 through 11 is picked up once Group A's spec exists to point at. Everything else on this roadmap has no issue yet, by design. + +--- + +# Pass 2026-09-11 — the Java web layer + +**Planned with:** Rahul Krishna + +Theme: **make the view layer of a Java web application reachable in the graph** — JSP, +servlets and their descriptors, Thymeleaf, JSF/Facelets. Prompted by an exploitability run +over DayTrader (`codeanalyzer-java` 3.2.0) whose findings skewed to build and deployment +files because the analyzer "read `src/main/java` only — JSP was never parsed." + +## The finding + +The claim is half right. The symbol table walks `src/*/java` roots only, but the +repository-artifact layer (#45) already inventories every JSP with its **full source** +(`format: text`, `roles: ["unknown"]`) and `web.xml` too (`roles: ["tool-config"]`, never +parsed). The bytes are in the graph; the structure is not. Measured on DayTrader: + +- 23 `.jsp` files against 141 `.java`; 348 `<%= %>` expressions (XSS sinks) and 14 + `request.getParameter` reads inside JSPs (taint sources) with no node of any kind; +- 33 distinct `.jsp` strings in the graph, all as **text** — servlets reach their views + through `getRequestDispatcher("/x.jsp").forward(...)` and the SDG stops at the string + literal, never at the artifact; +- no template is an entrypoint and no entrypoint has a URL: `web.xml` servlet-mappings and + welcome-files are unread, and Spring `@RequestMapping` arguments sit raw in decorators. + +The servlet side needs no new contract: `javaee/` already ships Jakarta, Spring, JAX-RS, +Struts and Camel entrypoint finders, and DayTrader's 151 entrypoints come from them. The +gap is entirely the view side — and it is not Java-specific. `codeanalyzer-python` has no +template role either (Jinja/Django) and `codeanalyzer-typescript` none for Vue/Svelte, so +whatever Java coins here is the vocabulary the other two inherit. + +## Candidates + +Numbering continues from the first pass. + +| # | Feature | Moves schema v2? | Collision group | Blocked by | +| - | ------- | ---------------- | --------------- | ---------- | +| 16 | **View-template artifact role** — `.jsp`, `.xhtml`, Thymeleaf `.html`, and the like classified instead of `unknown` | yes — the shared open `roles` vocabulary, which #48 says is itself unreconciled | D | #48 (folded in) | +| 17 | **View-dispatch edges** — `forward` / `include` / `sendRedirect` / `ModelAndView` / returned view name / JSF navigation outcome, from a callable or body node to the `Artifact`; view-name resolution needs the Spring/Thymeleaf prefix and suffix from config keys | yes — a new edge kind; `J_USES_CONFIG` is the precedent (language-prefixed source, un-prefixed target, `prov`) | D | 16 | +| 18 | **Descriptor-derived routes** — `web.xml` servlet/filter/welcome-file/error-page mappings and `faces-config.xml` navigation as a URL on the entrypoint, unified with annotation-derived routes (`@RequestMapping`, `@Path`) | yes — a route field on entrypoints; no analyzer has one | E | 6 (first pass) | +| 19 | **Template-internal code model** — how `<%= %>`, `${...}`, `th:*`, `#{...}` appear as nodes with spans beneath a non-code file: the node kind and the id grammar under an artifact | yes — a new kind in the shared ladder and a new id shape | F | 16 | +| 20 | **Template ↔ code dataflow** — `request.setAttribute` / `model.addAttribute` / managed-bean properties bound to template reads, and in-template `getParameter` as an SDG source | yes — `ddg` endpoints on template nodes, new `prov` values | F | 17, 19 | +| 21 | Bundler output inventory — `static/`, `dist/` JS and CSS as artifacts with a role | yes — same `roles` vocabulary as 16 | D | — | +| 22 | JSF and Thymeleaf entrypoint finders — `@Named` / `@ManagedBean`; `@Controller` already matches | **no** — an additive `entrypoint_frameworks` value | — | — | + +Candidate 22 is not a contract decision; it is a maintenance-rung item recorded so it is not +mistaken for one. It ships with 18. + +## Collision groups + +- **Group D — template-as-artifact vocabulary**: candidates 16, 17, 21. + + One role name and one edge name, coined once for three analyzers: python's + `render_template("x.html")` and TypeScript's SFC imports are the same edge from the same + role. The session **also settles #48** — the `roles` vocabulary is still contested between + the spec's closed `artifact_kind` and the shipped open `roles[]`, and a role coined on a + contested vocabulary is the one planning mistake the parity clause makes permanent. So the + spec amendment that resolves #48 and the spec that coins the template role are one PR. + +- **Group E — entrypoint route vocabulary**: candidate 18. + + The first pass's candidate 6 (entrypoint vocabulary, Group B) settled `is_entrypoint`, + `entrypoint_frameworks` and the entrypoint report; it did not give an entrypoint a URL. + Routes are where the microservice initiative's service boundaries begin, so this is the + hand-off point to that deferred work, decided once across descriptor-derived and + annotation-derived sources. One twist reaches back into Group D: a welcome-file JSP is + itself URL-addressable, so an `Artifact` can be an entrypoint, and E's field has to be + legal on D's node. + +- **Group F — template code model**: candidates 19, 20. + + One decision for JSP, Thymeleaf and Facelets; per-engine extractors are additive under + it. Jasper/JspC translation to a generated servlet is one option *inside* this session + (real callables, but spans that point at generated lines, not the JSP), not a candidate of + its own. + +## Dependency order + + #48 ──┐ + ├─▶ 16 role ──▶ 17 dispatch edges ──┐ + │ ├─▶ 20 template ↔ code dataflow + └─▶ 19 template code model ┘ + + 6 (first pass, Group B) ──▶ 18 routes [Group E; bridges to the microservice initiative] + + 21 bundler inventory — not now (below) + 22 finders — rides with 18, no contract + +Group D unblocks everything else on this pass and needs nothing new: every fact it +projects — the JSP text, the `web.xml` text, the string literal at the dispatch call site — +is already in the graph. Group E is independent of D and can run in parallel; Group F +cannot start until the role exists to hang nodes under. + +## Release trains + +| Train | Carries | Notes | +| ----- | ------- | ----- | +| `codellm-devkit/.github` spec | #48 resolution + 16, 17 | one spec PR; the vocabulary is what the other analyzers inherit | +| `codeanalyzer-java` 3.3.0 | 16, 17 | additive MINOR — a role value, an edge kind; `schema_version` stays 2.0.0 pending the coordinated re-baseline (#50) | +| `codeanalyzer-java` 3.4.0 | 18, 22 | additive MINOR | +| `codeanalyzer-java` next | 19, 20 | additive — new kind under an artifact; whether it is a MINOR depends on the id grammar Group F chooses | +| `python-sdk` | 16–20 as they land | the reconstructor and facade grow with each; a template node kind (19) is the first that needs a new model | +| `codeanalyzer-python`, `codeanalyzer-typescript` | 16, 17 vocabulary only | no work scheduled; they adopt the names when they grow templates (Not now) | + +## Not now + +- **Candidate 21, bundler output inventory** — inventory-only value; built JS is + `codeanalyzer-typescript`'s turf, and a Java repository that also carries a frontend is the + multi-language case the deferred microservice initiative owns. The role, if one is ever + coined, belongs to Group D's vocabulary and is recorded here so it is coined there. +- **Python and TypeScript templates** — Jinja/Django, Vue/Svelte SFCs. Same vocabulary, + different repos; they adopt Group D's names when they start, and nothing here schedules + them. +- **Freemarker / Velocity / Struts Tiles** — not in this pass's scope. Additive engines + under Group F's model if wanted later. +- **Frontend bundler analysis** (as opposed to inventory) — never Java's; TypeScript's. + +## Starting now + +**Group D — candidates 16 and 17, with #48 folded in.** Enters `designing-cldk-changes`; +the epic is filed when implementation starts, not before. + +Everything else on this pass has no issue yet, by design. From e80af07842655443b9013e14824152106483cf4c Mon Sep 17 00:00:00 2001 From: Rahul Krishna Date: Fri, 11 Sep 2026 08:57:11 -0400 Subject: [PATCH 2/3] docs(spec): view templates as artifacts, and the dispatch edge that reaches them Group D of the 2026-09-11 roadmap pass. Coins the view-template artifact role with its classification rules, the J_DISPATCHES_TO edge from a dispatching body node to the Artifact it reaches (via: forward | include | redirect | view-name | navigation; prov: literal | dataflow), and the view_dispatches / view_dispatches_unresolved lists on the application. Return body nodes carry their expression so the literal tier can see Spring view names. Declares the shipped artifact-layer vocabulary (format + roles[], dependency kind, per-analyzer ecosystem) canonical, closing the vocabulary item of #48. --- ...-09-11-java-view-templates-and-dispatch.md | 303 ++++++++++++++++++ 1 file changed, 303 insertions(+) create mode 100644 docs/design/specs/2026-09-11-java-view-templates-and-dispatch.md diff --git a/docs/design/specs/2026-09-11-java-view-templates-and-dispatch.md b/docs/design/specs/2026-09-11-java-view-templates-and-dispatch.md new file mode 100644 index 0000000..e0442d0 --- /dev/null +++ b/docs/design/specs/2026-09-11-java-view-templates-and-dispatch.md @@ -0,0 +1,303 @@ +# Spec: view templates as artifacts, and the dispatch edge that reaches them + +Status: draft for review +Date: 2026-09-11 +Scope: codeanalyzer-java (analyzer), python-sdk (Java facade, deferred), cross-repo vocabulary +Origin: roadmap pass 2026-09-11, Group D (candidates 16, 17); closes the vocabulary item of #48 + +--- + +## 1. Summary + +A Java web application's view layer is in the graph as bytes and absent as structure. The +repository-artifact layer already inventories every JSP, Facelet and Thymeleaf page with its full +`source`, but classifies them `roles: ["unknown"]`, and nothing connects a servlet or controller +to the page it renders: the SDG stops at the string literal inside +`getRequestDispatcher("/x.jsp")`. This spec adds two things, both additive: + +1. **A `view-template` artifact role**, with the classification rules that assign it, so a view + is findable at all. +2. **`J_DISPATCHES_TO`**, an edge from the body node that dispatches (a `forward` / `include` / + `sendRedirect` call, or a `return` in a controller) to the `Artifact` it dispatches to, with + `application.view_dispatches[]` / `view_dispatches_unresolved[]` in `analysis.json` — the same + level-graded, never-guessing shape `config_uses[]` already has. + +It also **declares the shipped artifact-layer vocabulary canonical**, which is what #48 asked for +(§ 6). A role coined on a contested vocabulary is the one mistake the parity clause makes +permanent, so the two decisions are one spec. + +### Measured on DayTrader (codeanalyzer-java 3.2.0, `--emit neo4j`) + +| | | +| --- | --- | +| view files inventoried, all `roles: ["unknown"]` | 23 `.jsp`, 15 `.xhtml`, 20 `.html` (against 141 `.java`) | +| descriptors inventoried, never parsed | `web.xml` (19 `url-pattern`s, 3 welcome files, 2 error pages), `faces-config.xml` | +| dispatch call sites | 22 `getRequestDispatcher`, 20 `include`, 2 `forward`, 1 `sendRedirect` | +| of which the target is a **string literal** | **5** — 3 JSPs, 2 servlet URLs (`/servlet/PingServlet2ServletRcv`, …), 1 `welcome.faces` | +| of which the target is a **variable** | **17** | +| `.jsp` strings anywhere in the graph, none as an edge | 33 distinct | +| `<%= %>` / in-JSP `request.getParameter` with no node of any kind | 348 / 14 | + +Two consequences shape the design. The literal tier alone reaches 3 of 23 JSPs, so the +dataflow tier is not optional for real code — exactly as it was not for config reads. And two of +the five literals are servlet URLs, not files: the edge must say plainly "this is not an artifact" +and hand those to the route work (roadmap Group E) rather than guess. + +### What this spec does not do + +- **No template-internal code.** `<%= %>`, `${…}`, `th:*`, `#{…}`, `` inside a + template — roadmap Group F. The role coined here is what Group F hangs its nodes under. +- **No routes.** `web.xml` servlet mappings, welcome files, `*.faces`, `@RequestMapping` paths — + Group E. A dispatch whose target is a URL rather than a file is recorded unresolved with reason + `no-such-artifact` so Group E can close it later. +- **No JSF outcome navigation.** `via: "navigation"` is reserved and its rule stated (§ 4.4), but + it activates only once a JSF entrypoint finder exists (roadmap candidate 22, rides with Group E); + without one, a `return "success"` in an arbitrary method cannot be told from a JSF action. +- **No new node kinds, no `schema_version` bump.** Everything is a role value, an edge kind, and a + populated-but-existing field. + +--- + +## 2. Contract-Impact Triage + +| Question | Answer | +| --- | --- | +| Does this change the schema v2 output? | **Yes, additively.** One `roles` value; three `format` values (`jsp`, `xhtml`, `html`); one edge kind `J_DISPATCHES_TO`; two lists on `application`; `argument_expr` populated on `return` body nodes (the field already exists on every body node, empty on returns today). | +| Repos touched | `codeanalyzer-java` (emits); `python-sdk` (models + Java facade + Neo4j reconstruct); `codeanalyzer-python` / `codeanalyzer-typescript` (**vocabulary only** — they adopt `view-template` and `PY_DISPATCHES_TO` / `TS_DISPATCHES_TO` when they grow templates; no work scheduled). | +| Change type | Schema v2 evolution, additive at the leaves. | + +--- + +## 3. Where the analyzers stand today + +| | codeanalyzer-python 1.5.1 | codeanalyzer-typescript 1.5.3 | codeanalyzer-java 3.2.0 | +| --- | --- | --- | --- | +| `roles` vocabulary | `dependency-manifest service-topology container-image ci env tool-config packaging script docs legal iac unknown` | same set | same set (`artifacts/ArtifactDiscovery.java:48-90`) | +| a template role | none (Jinja/Django → `unknown`) | none (`.vue`/`.svelte` → `unknown`) | none (`.jsp`/`.xhtml`/`.html` → `unknown`) | +| code → artifact edge precedent | `PY_USES_CONFIG` from `PyBodyNode` to `ConfigKey`, `prov: string[]` | `TS_USES_CONFIG`, same shape | `J_USES_CONFIG` from `JBodyNode ∪ JCallable ∪ JField ∪ JType` (annotations have no body node) | +| unresolved reads | `config_reads_unresolved[]` `{site, callee, key, reason, prov}` | same | same | +| `return` body node payload | — | — | span only; `argument_expr: []` | + +The vocabulary is identical across the three, which is the fact #48 needed established before +anything was added to it. + +--- + +## 4. Design decisions + +### D1. The role is `view-template`; `format` names the syntax + +Hyphenated like every multi-word role already shipped. `template` alone was rejected because +codeanalyzer-iac (#52) will need the word for Helm templates; `view` alone loses the fact that the +file carries embedded code, which is what Group F depends on. + +Classification rules, first-match-wins, inserted **before** the generic `*.xml` row: + +| Pattern | `format` | `roles` | +| --- | --- | --- | +| `*.jsp`, `*.jspx`, `*.jspf`, `*.tag`, `*.tagx` | `jsp` | `view-template` | +| `*.xhtml` | `xhtml` | `view-template` | +| `*/templates/*.html`, `*/WEB-INF/*.html` | `html` | `view-template` | +| `faces-config.xml` | `xml` | `tool-config` | + +`*` crosses `/` in this matcher (`ArtifactDiscovery.globMatches`), so `*/templates/*.html` covers +`src/main/resources/templates/admin/users.html`. A bare `*.html` anywhere else stays `unknown`: a +static page and a Thymeleaf template are not distinguishable by name (DayTrader's 20 `.html` are +WebSocket test pages), and misclassifying a static page as a template would later hand Group F a +file with no expressions to extract. `faces-config.xml` is added to the descriptor rows for the +same reason `web.xml` is there — it is read by § 4.4, and a consumer should find it as +`tool-config` rather than `unknown`. + +The three `format` values are new but the field is open (python already has nine, TS ten, no two +lists equal), so this coins nothing that needs a sibling to agree. + +### D2. One edge, `J_DISPATCHES_TO`, with `via` carrying the mechanism + +``` +(:JBodyNode)-[:J_DISPATCHES_TO {via: string, prov: string[]}]->(:Artifact) +``` + +- **`from` is `JBodyNode` only.** Every dispatch site is a body node: the `forward` / `include` / + `sendRedirect` / `setViewName` / `new ModelAndView(…)` call node, or the `return` node itself + once it carries its expression (D3). Unlike `J_USES_CONFIG`, there is no annotation-shaped + source to force a union. +- **`to` is `Artifact`**, un-prefixed, the cross-language merge target. The edge is `J_`-prefixed + because the source is Java code — same reasoning as `J_USES_CONFIG`; python coins + `PY_DISPATCHES_TO` for `render_template` / `redirect` when it gets there. +- **`via`** ∈ `forward | include | redirect | view-name | navigation`. One edge with a discriminant + rather than `J_RENDERS` + `J_REDIRECTS_TO`: the consumer question is "what pages can this + code reach", and the mechanism is an attribute of the answer, not a different answer. +- **`prov`** ∈ `literal | dataflow`, the tier that resolved it, monotone with the analysis level + exactly as on `J_USES_CONFIG`: `["literal"]` at L1, `"dataflow"` when the target reached the + site over the L3 DDG (17 of DayTrader's 22 dispatches need it). +- **MERGE on the endpoint pair.** A body node dispatches to one target; a `return` on two paths of + one method is two body nodes. No `_k` discriminant is needed. + +Which Java calls count, and what the literal is: + +| Site | `via` | target expression | +| --- | --- | --- | +| `RequestDispatcher.forward(req, res)` | `forward` | the argument of the `getRequestDispatcher(…)` / `getNamedDispatcher(…)` call that produced the receiver | +| `RequestDispatcher.include(req, res)` | `include` | same | +| `HttpServletResponse.sendRedirect(x)` | `redirect` | `x` | +| `return x` in a Spring controller method (D3) | `view-name` | `x` | +| `new ModelAndView(x, …)`, `ModelAndView.setViewName(x)` | `view-name` | `x` | +| Spring view name with `redirect:` / `forward:` prefix | `redirect` / `forward` | the remainder | + +The `forward` / `include` rows anchor on the **dispatching** call (that is the node whose +execution reaches the page), and read the target off the `getRequestDispatcher` call node one +receiver up — `receiver_expr` already carries the full chain +(`ctx.getRequestDispatcher("/quoteDataPrimitive.jsp")`), and the dispatcher's own body node +carries `argument_expr: ['"/quoteDataPrimitive.jsp"']`. + +### D3. `return` body nodes carry their expression + +`argument_expr` is set to `[]` on every `return` node with a value, and stays +`[]` for a bare `return`. The field already exists on every `JBodyNode` (`string[]`), so this is +a population change, not a schema addition; it is also the smallest fact that lets the literal +tier see `return "home"` at all, and it benefits any later dataflow over returns. The +alternatives — a callable-level edge fed only by the dataflow tier, or deferring Spring view +names to Group F — both left Thymeleaf with a role and no edges, and Thymeleaf's only dispatch +mechanism *is* the Spring view name. + +### D4. Resolution never guesses + +A target expression resolves to an artifact, or is recorded unresolved with a reason. There is no +"closest match". + +**Path targets** (`forward`, `include`, `redirect`, and view names after prefix stripping): the +literal is a servlet-context-relative path. It resolves iff exactly one artifact's repo-relative +path ends with the webapp-relative form of it (`/marketSummary.jsp` → +`src/main/webapp/marketSummary.jsp`); a leading context path (`/daytrader/…`) is not stripped — +that is a route fact, Group E's. + +**View names** (`via: "view-name"`), gated to callables whose type or callable carries `spring` +in `entrypoint_frameworks` and whose return type is `String` or `ModelAndView`, so a `return +"foo"` in ordinary code never matches a template named `foo.html`: + +1. `redirect:` / `forward:` prefixes re-dispatch as path targets with the corresponding `via`. +2. Otherwise the candidate path is ``, where prefix/suffix come from the + application's own `ConfigKey`s when present as literals — `spring.mvc.view.prefix` / `.suffix` + for JSP, `spring.thymeleaf.prefix` / `.suffix` for Thymeleaf — and from Thymeleaf's defaults + (`classpath:/templates/`, `.html`) when absent. `classpath:` and `/WEB-INF/` are matched + against any resource or webapp root, as the runtime would. +3. It resolves iff exactly one `view-template` artifact matches; two or more is `ambiguous`. + +**JSF** (`via: "navigation"`, reserved): explicit `faces-config.xml` `navigation-rule` +`from-outcome → to-view-id`, then implicit `.xhtml`, gated to callables in a JSF managed +bean — which needs the finder that roadmap candidate 22 adds. Until then nothing is emitted with +this `via`, and the rule is here so it is coined once. + +**Unresolved reasons**, mirroring `config_reads_unresolved`: + +| `reason` | when | +| --- | --- | +| `non-literal` | the target never closed on a string at any attempted tier (`prov` lists the tiers tried) | +| `no-such-artifact` | a literal that maps to no inventoried file — servlet URLs (`/servlet/PingServlet2ServletRcv`), `*.faces`, absolute URLs, a template outside the repo | +| `ambiguous` | a view name matching more than one `view-template` artifact | + +--- + +## 5. Wire format + +### `analysis.json` + +```jsonc +"application": { + "view_dispatches": [ + { "src": "can://…/PingServlet2Jsp.java/PingServlet2Jsp/doGet(…)@69:13", + "dst": "can://daytrader8/artifact/src/main/webapp/PingServlet2Jsp.jsp", + "via": "forward", "prov": ["literal"] } + ], + "view_dispatches_unresolved": [ + { "site": "can://…/PingServlet2Servlet.java/PingServlet2Servlet/doGet(…)@73:13", + "callee": "forward(javax.servlet.ServletRequest, javax.servlet.ServletResponse)", + "target": "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/servlet/PingServlet2ServletRcv", "via": "forward", + "reason": "no-such-artifact", "prov": ["literal"] }, + { "site": "…@88:9", "callee": "include(…)", "target": null, "via": "include", + "reason": "non-literal", "prov": ["literal"] } + ] +} +``` + +Both lists are absent (not `[]`) when the layer produced nothing, matching `config_uses`. `target` +is the literal when there was one and `null` for `non-literal`. Every `return` body node gains +`argument_expr: [""]` where a value is returned. + +### Neo4j (`V2SchemaCatalog`) + +``` +J_DISPATCHES_TO from [JBodyNode] to [Artifact] { via: string, prov: string[] } +``` + +Unresolved dispatches are not projected as relationships — there is no target node — and stay in +`analysis.json`, as `config_reads_unresolved` does except for its `JExternal` form; whether an +`Artifact`-less form is worth a relationship is an open question (§ 9). `Artifact` gains no +property; `roles` is already `string[]`. `schema_version` stays 2.0.0 (#50). + +--- + +## 6. What this closes on #48 + +#48 lists three vocabularies the artifact-layer spec mandated and no analyzer implemented, and +recommends option 1, "amend the spec to what shipped". That spec exists only on the unmerged +branch `feat/configuration-files`; on `main` this document is the first to state the artifact +vocabulary, and it states the shipped one as the contract: + +- **`format: string` + `roles: string[]`**, open, replacing the closed `artifact_kind` enum. + `roles` is many-valued on purpose (`build.gradle` is both `dependency-manifest` and + `tool-config`), which a single enum cannot express. +- **Dependency `kind` ∈ `runtime | dev | optional | build`**, replacing `scope`. +- **`ecosystem` is a per-analyzer literal** (`pypi`, `npm`, `maven`), not a shared table. + +Epic #45's "Contract as shipped" section already says the same; this makes it a committed spec +rather than an issue comment, and `view-template` is the first addition made under it. Errata +items #48 later accumulated — the `@key:env/` id grammar and the orphan sweep on re-push — are +**not** closed here; they are cross-analyzer contract questions of their own and #48 stays open +for them. + +--- + +## 7. SDK surface (python-sdk, deferred to its own PR) + +Mirrors the config-use surface exactly, so nothing new has to be learned: + +- models `JViewDispatch {src, dst, via, prov}` and `JViewDispatchUnresolved {site, callee, + target, via, reason, prov}`; `JApplication.view_dispatches` / `.view_dispatches_unresolved`, + both `Optional[List[…]] = None`. +- `JavaAnalysis.get_view_dispatches(view: str | None = None)` and + `get_view_dispatchers(path: str) -> List[JCallableOverview]`, alongside `get_config_uses` / + `get_config_readers`. +- the Neo4j reconstructor reads `J_DISPATCHES_TO` back into `view_dispatches`; unresolved + entries are JSON-only, as `config_reads_unresolved` is today. + +`JArtifact` is unchanged — `roles` was already an open list. + +--- + +## 8. Decomposition and release plan + +Decided 2026-09-11: **a single work item on `codeanalyzer-java`**, with the SDK follow-up as a +definition-of-done line rather than a child issue. No epic: the cross-repo part of this change is +vocabulary, recorded here, and only one repo has a PR to ship. + +| Train | Carries | Notes | +| --- | --- | --- | +| `codeanalyzer-java` 3.3.0 | D1–D4, § 5 | additive MINOR; `schema.neo4j.json` regenerated; parity gate covers the new list | +| `python-sdk` next rc | § 7 | after 3.3.0 is on PyPI; pins move with it | +| `codeanalyzer-python`, `codeanalyzer-typescript` | — | no train; adopt the names when they grow templates | + +--- + +## 9. Open questions for review + +1. **Unresolved dispatches in the graph.** `config_reads_unresolved` projects to + `J_READS_CONFIG_UNRESOLVED` on a `JExternal`; a `no-such-artifact` dispatch has an equally + real target (a servlet URL). Projecting it now would pre-empt Group E's route node; leaving it + JSON-only means a Cypher consumer cannot see it. This spec leaves it JSON-only. +2. **`faces-config.xml` `from-view-id → to-view-id`** is an `Artifact → Artifact` edge with no + code in between. It is template-to-template navigation, which is Group F's shape, and is not + emitted here. +3. **`*.html` under the webapp root.** Left `unknown` by design (D1). If a project keeps Thymeleaf + templates outside `templates/` and `WEB-INF/`, the fix is a configuration knob on the rule, not + a broader default. From 76a8c76bd6cb7da4a791b8b6b7c8bf8edbc28873 Mon Sep 17 00:00:00 2001 From: Rahul Krishna Date: Fri, 11 Sep 2026 10:20:34 -0400 Subject: [PATCH 3/3] docs(spec): may-dispatch over a static string table, prov table (codeanalyzer-java#261) --- ...-09-11-java-view-templates-and-dispatch.md | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/docs/design/specs/2026-09-11-java-view-templates-and-dispatch.md b/docs/design/specs/2026-09-11-java-view-templates-and-dispatch.md index e0442d0..00a71dd 100644 --- a/docs/design/specs/2026-09-11-java-view-templates-and-dispatch.md +++ b/docs/design/specs/2026-09-11-java-view-templates-and-dispatch.md @@ -127,7 +127,7 @@ lists equal), so this coins nothing that needs a sibling to agree. - **`via`** ∈ `forward | include | redirect | view-name | navigation`. One edge with a discriminant rather than `J_RENDERS` + `J_REDIRECTS_TO`: the consumer question is "what pages can this code reach", and the mechanism is an attribute of the answer, not a different answer. -- **`prov`** ∈ `literal | dataflow`, the tier that resolved it, monotone with the analysis level +- **`prov`** ∈ `literal | table | dataflow`, the tier that resolved it (§ 4.5 for `table`), monotone with the analysis level exactly as on `J_USES_CONFIG`: `["literal"]` at L1, `"dataflow"` when the target reached the site over the L3 DDG (17 of DayTrader's 22 dispatches need it). - **MERGE on the endpoint pair.** A body node dispatches to one target; a `return` on two paths of @@ -188,6 +188,28 @@ in `entrypoint_frameworks` and whose return type is `String` or `ModelAndView`, bean — which needs the finder that roadmap candidate 22 adds. Until then nothing is emitted with this `via`, and the rule is here so it is coined once. +### 4.5. May-dispatch over a static string table — `prov: ["table"]` (added 2026-09-11, codeanalyzer-java#261) + +Running 3.3.0 on DayTrader resolved 3 of 23 dispatches; the rest go through +`TradeConfig.getPage(N)` = `return webUI[webInterface][pageNumber]`, a static `String[][]` of page +paths indexed by a runtime-selected interface. No single target exists on any path, so the tiers +above refuse correctly — and the 20 remaining JSPs stay unreachable although the set of pages the +code can reach is fully static. The **table tier** closes that shape deliberately as a +may-dispatch: a target expression that is a call `T.m(…)` whose every `return` is an array access +rooted at a static `String[]` / `String[][]` field of `T` with an array-initializer of string +literals resolves to **every literal in that initializer**, one `J_DISPATCHES_TO` per matching +artifact, `prov: ["table"]`. The same closure applies to a caller's argument when the +interprocedural tier binds a parameter; the union of all callers is the candidate set and `prov` +is `["table"]` when any table contributed. + +This is the first over-approximation in the pass, and `prov` is what keeps it honest: `literal` and +`dataflow` edges still mean exactly one target; a `table` edge means "one of these". Table entries +naming no artifact (a servlet URL) contribute nothing; a table matching none is +`no-such-artifact`. Scope is the table shape only — not constant propagation, maps, enums, or +`switch`-returned literals. The callee is found by declaring-type simple name plus method name +within the tree, so the tier runs at `-a 1`; two same-named types both declaring the method make +it give up rather than pick one. + **Unresolved reasons**, mirroring `config_reads_unresolved`: | `reason` | when |