@heshammesalam/heshjs 0.2.0 → 0.3.0

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.
Files changed (45) hide show
  1. package/README.md +8 -2
  2. package/cli.js +26 -0
  3. package/dist/architecture/diagnostics.d.ts +12 -0
  4. package/dist/architecture/diagnostics.js +8 -0
  5. package/dist/architecture/diagnostics.js.map +1 -0
  6. package/dist/architecture/html.d.ts +16 -0
  7. package/dist/architecture/html.js +132 -0
  8. package/dist/architecture/html.js.map +1 -0
  9. package/dist/architecture/index.d.ts +42 -0
  10. package/dist/architecture/index.js +418 -0
  11. package/dist/architecture/index.js.map +1 -0
  12. package/dist/compiler/index.js +2 -0
  13. package/dist/compiler/index.js.map +1 -1
  14. package/dist/destinations/index.d.ts +63 -0
  15. package/dist/destinations/index.js +185 -0
  16. package/dist/destinations/index.js.map +1 -0
  17. package/dist/dom/index.d.ts +2 -0
  18. package/dist/dom/index.js +5 -1
  19. package/dist/dom/index.js.map +1 -1
  20. package/dist/domains/index.d.ts +24 -0
  21. package/dist/domains/index.js +110 -0
  22. package/dist/domains/index.js.map +1 -0
  23. package/dist/parser/index.d.ts +15 -1
  24. package/dist/parser/index.js +73 -3
  25. package/dist/parser/index.js.map +1 -1
  26. package/dist/presentation/index.d.ts +9 -0
  27. package/dist/presentation/index.js +38 -0
  28. package/dist/presentation/index.js.map +1 -0
  29. package/dist/vite/index.d.ts +4 -1
  30. package/dist/vite/index.js +36 -2
  31. package/dist/vite/index.js.map +1 -1
  32. package/docs/contracts.md +60 -0
  33. package/docs/domain-graph.md +205 -0
  34. package/docs/grammar.md +4 -0
  35. package/package.json +8 -3
  36. package/src/architecture/diagnostics.ts +11 -0
  37. package/src/architecture/html.ts +88 -0
  38. package/src/architecture/index.ts +283 -0
  39. package/src/compiler/index.ts +1 -0
  40. package/src/destinations/index.ts +146 -0
  41. package/src/dom/index.ts +6 -1
  42. package/src/domains/index.ts +97 -0
  43. package/src/parser/index.ts +55 -4
  44. package/src/presentation/index.ts +31 -0
  45. package/src/vite/index.ts +24 -2
@@ -0,0 +1,205 @@
1
+ # Domain Graph first vertical slice (0.3)
2
+
3
+ This opt-in architecture builds on the existing Hesh compiler, shallow reactivity,
4
+ DOM renderer, router and HMVC. Existing components and `createRouter` remain valid.
5
+ This is the first architecture milestone, not an enterprise-ready framework.
6
+ These APIs require 0.3 or later. Install `@heshammesalam/heshjs@0.3.0` in an
7
+ existing Vite application and enable the architecture option described below.
8
+ The npm creator's default template remains HMVC; it does not scaffold domains.
9
+ The following commands run the reference examples from the framework repository:
10
+
11
+ ```sh
12
+ npm install
13
+ npm run check:architecture
14
+ npm run dev:domains # JS Identity/Inventory/Finance demo, localhost:5180
15
+ npm run dev:domains:ts # strict TS alternative, localhost:5181
16
+ ```
17
+
18
+ The JS demo uses mock identity, an abortable timer-backed mock product API and a
19
+ mock tax service. No HTTP server, credentials, payments or real authorization are
20
+ implemented. Browser refresh restores destination/path input, not in-memory demo
21
+ login state. Journeys are deliberately not simulated.
22
+
23
+ ## Descriptors and real implementations
24
+
25
+ ```hesh
26
+ domain Inventory {
27
+ implementation: "./inventory.js#createInventory";
28
+ owns { Product; }
29
+ exposes { products.get; stock.reserve; }
30
+ consumes { Identity.session; }
31
+ access { stock.reserve: "inventory.reserve"; }
32
+ }
33
+ presentation ProductScreen {
34
+ html: "./ui/product.html";
35
+ logic: "./ui/product.js#createProduct";
36
+ styles: "./ui/product.css";
37
+ bindings { title; }
38
+ actions { reserve; }
39
+ }
40
+ destination ProductDetails {
41
+ url: "/products/:id";
42
+ input { id: string; }
43
+ presentation: ProductScreen;
44
+ access: "inventory.read";
45
+ }
46
+ ```
47
+
48
+ ```js
49
+ // inventory.js: private implementation, no imports from another domain's files.
50
+ export function createInventory(dependencies) {
51
+ return {
52
+ products: { get: id => ({ id, title: 'Mock product' }) },
53
+ stock: { reserve: id => dependencies.Identity.session() + ': ' + id }
54
+ };
55
+ }
56
+ ```
57
+
58
+ `owns` is metadata, not a generated entity/database/validation system. Exposes are
59
+ checked against the real factory return type. Consumers receive only the listed
60
+ public operations, not the whole implementation object. Factories execute once
61
+ per application domain graph in dependency order. Cycles and missing contracts
62
+ are errors. Dependencies and public facades are frozen; disposed graphs reject
63
+ further operation calls. Factories may use `onCleanup` for owned resources.
64
+
65
+ The application descriptor imports domain files explicitly and selects names:
66
+
67
+ ```hesh
68
+ import "./domains/identity/domain.hesh";
69
+ import "./domains/inventory/domain.hesh";
70
+ application Shop {
71
+ domains { Identity; Inventory; }
72
+ destinations { ProductDetails; }
73
+ }
74
+ ```
75
+
76
+ Every selected destination/presentation belongs to one selected domain directory.
77
+ Each domain has its own directory boundary. Shared code can be imported by domains;
78
+ shared code cannot re-export a private domain implementation as an escape hatch.
79
+
80
+ ## Standard HTML, CSS and explicit bindings
81
+
82
+ ```html
83
+ <section aria-labelledby="title">
84
+ <h1 id="title">{title}</h1>
85
+ <button h-action="reserve">Reserve</button>
86
+ </section>
87
+ ```
88
+
89
+ ```js
90
+ import { signal } from '@heshammesalam/heshjs';
91
+ export function createProduct({ domains, input, location, signal: cancellation }) {
92
+ const title = signal(domains.Inventory.products.get(input.id).title);
93
+ return {
94
+ bindings: { title },
95
+ actions: { reserve() { domains.Inventory.stock.reserve(input.id); } }
96
+ };
97
+ }
98
+ ```
99
+
100
+ Bindings are explicit declared data paths, not arbitrary evaluated JS strings.
101
+ `{product.name}` and `h-bind="product.name"` use text nodes. `\{`/`\}` produce
102
+ literal braces. A whole attribute binding like `disabled="{disabled}"` preserves
103
+ boolean values; mixed attribute text is a string. Standard native HTML/custom tags
104
+ are preserved without Hesh element aliases. Browser forms, labels, ARIA and native
105
+ validation remain available. `h-action` means click, or submit on a form (preventing
106
+ full submission). Events are passed to actions and async errors use onError.
107
+ `h-value="email"` is two-way binding to a root writable signal on text controls.
108
+
109
+ `h-destination="ProductDetails" h-input-id="productId"` on a native anchor builds
110
+ a real href from the destination contract. Input attributes follow HTML's case
111
+ insensitivity; destination input names must therefore be unique ignoring case.
112
+ Missing/extra/wrongly typed inputs, missing bindings/actions and unsafe directives
113
+ produce located diagnostics. Dynamic any-typed data emits warnings when the shape
114
+ cannot be proven. JS receives inferred checks/JSDoc where present and runtime
115
+ input validation; this does not make arbitrary JavaScript fully type-safe.
116
+
117
+ HTML is parsed with parse5 at build time and emitted as DOM instructions. No
118
+ innerHTML/eval/Function constructor is used. Script/embedding/foreign/template
119
+ elements, inline event attributes and unknown h-* directives are rejected. SVG,
120
+ keyed h-each, raw HTML and scoped CSS are not implemented in this slice. Standard
121
+ CSS is imported unchanged through Vite, including variables, modern selectors,
122
+ media/container queries, animations and global themes. CSS is global in this release.
123
+
124
+ ## Generated typed destinations
125
+
126
+ Configure Vite using `hesh({ architecture: { root: 'src' } })`. For a tsconfig
127
+ outside src add `tsconfig: '../tsconfig.json'`. The plugin validates before build
128
+ and generates `src/.hesh-generated/app.js`, app.d.ts and compiled presentation
129
+ modules. Do not hand-edit generated files. Invalid checks block build; last valid
130
+ files are not deleted, so bypassing the check pipeline can use stale output.
131
+
132
+ ```js
133
+ import { createApplication } from './.hesh-generated/app.js';
134
+ const app = createApplication({
135
+ authorize: policy => policy === 'inventory.read', // CLIENT ONLY
136
+ onError: error => console.error(error),
137
+ onDiagnostic: event => console.debug(event.type, event.destination, event.reason)
138
+ });
139
+ const dispose = app.start(document.querySelector('#app'));
140
+ app.destinations.ProductDetails.open({ id: '15' });
141
+ app.destinations.ProductDetails.replace({ id: '20' }, { query: { tab: 'stock' } });
142
+ await app.destinations.ProductDetails.prefetch({ id: '30' });
143
+ ```
144
+
145
+ Generated declarations give each destination its exact required input fields and
146
+ primitive types. Empty-input destinations use `Record<string, never>`. Runtime
147
+ validation also rejects unknown/missing fields, non-finite numbers, empty strings
148
+ and URL dot segments. Inputs are encoded once and decoded once; URL parameter
149
+ names currently use the existing router's ASCII letter/digit/underscore rules.
150
+ Overlapping destination patterns are rejected so an input cannot silently open a
151
+ different static destination. This restriction applies only to the new contracts;
152
+ the existing router retains its static-priority behavior.
153
+
154
+ Page is the only supported presentation mode. Destinations load their own compiled
155
+ presentation and logic resources lazily. Prefetch deduplicates those code loads;
156
+ it does not fetch business data or authorize operations. Navigation disposes the
157
+ previous presentation, aborts its signal and prevents a late loader from mounting
158
+ stale UI. Dynamic module downloads themselves cannot be canceled. Factories are
159
+ synchronous and start their own asynchronous work using the supplied signal.
160
+ Guard against late results when using non-abortable services. Query/hash changes
161
+ create a fresh presentation instance. Successful mounting focuses a heading or
162
+ `data-hesh-focus`; scroll restoration and modal focus trapping are deferred.
163
+
164
+ `app.status.get()` is reactive loading/ready/error state. Optional diagnostic
165
+ events identify loading, ready, blocked, errors, cancellation and disposal. They
166
+ do not infer business intent or explain arbitrary reactive dependency chains.
167
+ Client policies default to deny when declared without an authorize callback;
168
+ server-side enforcement is still necessary for every protected operation.
169
+
170
+ ## Architecture validation and graph tooling
171
+
172
+ ```sh
173
+ hesh check ./src --write
174
+ hesh check ./src --tsconfig ../tsconfig.json --write
175
+ hesh graph ./src
176
+ ```
177
+
178
+ From the repository use `node packages/heshjs/cli.js ...`. Root means the directory
179
+ containing app.hesh, typically src. The TS resolver handles aliases, relative paths,
180
+ type imports and re-exports. Canonical paths account for filesystem aliases and
181
+ symlinks. Physical cross-domain imports of implementations are private, even if
182
+ the target JS module exports a function; use declared injection instead. Node and
183
+ server-only modules are blocked in client code. Computed imports in application
184
+ source are rejected because their target cannot be validated. This is build-time
185
+ architecture enforcement for configured projects, not a sandbox against malicious
186
+ application code, reflection or uninspected third-party packages.
187
+
188
+ Diagnostics include code, severity, path, offset/line/column, explanation and a
189
+ suggestion. HTML locations identify their source node/attribute; entity decoding
190
+ can make the exact inner text offset approximate. Graph JSON contains domain
191
+ dependencies, destination ownership/input schemas and compiled navigation links.
192
+ Descriptor ASTs are reused when unchanged and output writes are content-based.
193
+ Semantic analysis currently rechecks the whole graph; precise dependency-aware
194
+ typecheck invalidation and full HTML/Hesh source maps are deferred. The Vite workflow
195
+ uses full reload rather than state-preserving HMR.
196
+
197
+ ## Deliberate limits
198
+
199
+ Journeys, modal/panel/nested destination modes, redirects, scroll restoration,
200
+ file routing, generic async resources/caching, deep collections, scoped CSS,
201
+ server authorization, SSR/hydration, advanced scaffolding commands and a language
202
+ server are not implemented. Existing HMVC nested routes remain usable separately.
203
+ Production hosts still need SPA fallback to index.html for direct URLs. No
204
+ performance superiority, globally unique architecture or enterprise-readiness
205
+ claim is made. See architecture/domain-graph-audit.md for migration order.
package/docs/grammar.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Milestone 1 language (v0.1)
2
2
 
3
+ This remains the legacy component grammar. The opt-in architecture descriptor
4
+ extension is specified separately in [contracts.md](contracts.md); it does not
5
+ replace existing .hesh views or add template directives to the brace UI syntax.
6
+
3
7
  The grammar is deliberately smaller than the product roadmap. Source is UTF-8.
4
8
  Whitespace and JavaScript comments separate tokens. Identifiers follow JavaScript
5
9
  identifier rules. Strings and numbers follow JavaScript lexical rules.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heshammesalam/heshjs",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "An experimental independent reactive UI framework with brace-based .hesh components",
5
5
  "author": "Hesham",
6
6
  "license": "UNLICENSED",
@@ -16,11 +16,16 @@
16
16
  "./vite": { "types": "./dist/vite/index.d.ts", "import": "./dist/vite/index.js" },
17
17
  "./router": { "types": "./dist/router/index.d.ts", "import": "./dist/router/index.js" },
18
18
  "./hmvc": { "types": "./dist/hmvc/index.d.ts", "import": "./dist/hmvc/index.js" },
19
+ "./domains": { "types": "./dist/domains/index.d.ts", "import": "./dist/domains/index.js" },
20
+ "./destinations": { "types": "./dist/destinations/index.d.ts", "import": "./dist/destinations/index.js" },
21
+ "./presentation": { "types": "./dist/presentation/index.d.ts", "import": "./dist/presentation/index.js" },
22
+ "./architecture": { "types": "./dist/architecture/index.d.ts", "import": "./dist/architecture/index.js" },
19
23
  "./package.json": "./package.json"
20
24
  },
21
- "files": ["dist", "src", "docs", "README.md"],
25
+ "files": ["dist", "src", "docs", "README.md", "cli.js"],
26
+ "bin": { "hesh": "./cli.js" },
22
27
  "engines": { "node": ">=22.12.0" },
23
- "dependencies": { "typescript": "~5.9.3" },
28
+ "dependencies": { "typescript": "~5.9.3", "parse5": "8.1.0" },
24
29
  "peerDependencies": { "vite": "^7.2.2" },
25
30
  "peerDependenciesMeta": { "vite": { "optional": true } },
26
31
  "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" }
@@ -0,0 +1,11 @@
1
+ export interface ArchitectureDiagnostic {
2
+ code: string; severity: 'error' | 'warning'; message: string;
3
+ file: string; offset: number; line: number; column: number; suggestion: string;
4
+ }
5
+ export function diagnostic(code: string, message: string, file: string, source: string, offset: number, suggestion: string, severity: 'error' | 'warning' = 'error'): ArchitectureDiagnostic {
6
+ const lines = source.slice(0, offset).split('\n');
7
+ return { code, message, file, offset, line: lines.length, column: lines.at(-1)!.length + 1, suggestion, severity };
8
+ }
9
+ export function formatDiagnostic(item: ArchitectureDiagnostic): string {
10
+ return `${item.file}:${item.line}:${item.column} ${item.code} ${item.severity}: ${item.message}\n ${item.suggestion}`;
11
+ }
@@ -0,0 +1,88 @@
1
+ import { parseFragment, type DefaultTreeAdapterMap } from 'parse5';
2
+ import { diagnostic, type ArchitectureDiagnostic } from './diagnostics.js';
3
+ export interface HTMLContract {
4
+ filename: string; bindings: string[]; actions: string[];
5
+ destinations: Record<string, Record<string, string>>;
6
+ validateBinding?: (path: string, offset: number, expected?: string, writable?: boolean) => void;
7
+ }
8
+ export interface HTMLResult { code: string; diagnostics: ArchitectureDiagnostic[]; usedBindings: string[]; usedActions: string[]; destinations: string[] }
9
+ export function compileHTML(source: string, contract: HTMLContract): HTMLResult {
10
+ const diagnostics: ArchitectureDiagnostic[] = [], usedBindings = new Set<string>(), usedActions = new Set<string>();
11
+ const report = (code: string, message: string, at: number, suggestion: string, severity: 'error' | 'warning' = 'error') => diagnostics.push(diagnostic(code, message, contract.filename, source, at, suggestion, severity));
12
+ const fragment = parseFragment(source, { sourceCodeLocationInfo: true, onParseError: error => report('HESH-HTML-001', error.code, error.startOffset, 'Correct the HTML syntax.') });
13
+ const lines = ["import * as p from '@heshammesalam/heshjs/presentation';", 'export default function render(parent, state, destinations) {'];
14
+ let serial = 0;
15
+ const linked = new Set<string>();
16
+ const path = (value: string, offset: number, expected?: string, writable = false): string => {
17
+ const pieces = value.trim().split('.');
18
+ if (pieces.some(piece => !/^[$_\p{ID_Start}][$_\u200c\u200d\p{ID_Continue}]*$/u.test(piece) || ['constructor', '__proto__', 'prototype'].includes(piece))) report('HESH-BIND-001', `Invalid binding path ${value}`, offset, 'Use a declared dotted data path, not an expression.');
19
+ if (!contract.bindings.includes(pieces[0]!)) report('HESH-BIND-002', `Missing data binding ${pieces[0]}`, offset, 'Declare the binding and return it from the presentation factory.');
20
+ usedBindings.add(pieces[0]!); contract.validateBinding?.(value.trim(), offset, expected, writable);
21
+ return `p.readBinding(state.bindings, ${JSON.stringify(value.trim())})`;
22
+ };
23
+ const interpolation = (value: string, offset: number): string => {
24
+ const parts: string[] = []; let literal = '', cursor = 0;
25
+ const emit = () => { if (literal) { parts.push(JSON.stringify(literal)); literal = ''; } };
26
+ while (cursor < value.length) {
27
+ if (value[cursor] === '\\' && ['{', '}'].includes(value[cursor + 1] ?? '')) { literal += value[cursor + 1]; cursor += 2; continue; }
28
+ if (value[cursor] !== '{') { literal += value[cursor++]; continue; }
29
+ emit(); const start = ++cursor, end = value.indexOf('}', start);
30
+ if (end < 0) { report('HESH-BIND-001', 'Unterminated interpolation', offset + start - 1, 'Close the binding with }.'); break; }
31
+ parts.push(`String(${path(value.slice(start, end), offset + start)} ?? '')`); cursor = end + 1;
32
+ }
33
+ emit(); return parts.join(' + ') || "''";
34
+ };
35
+ const visit = (node: DefaultTreeAdapterMap['childNode'], parent: string): void => {
36
+ const at = node.sourceCodeLocation?.startOffset ?? 0;
37
+ if (node.nodeName === '#text') { lines.push(`p.text(${parent}, () => ${interpolation((node as DefaultTreeAdapterMap['textNode']).value, at)});`); return; }
38
+ if (!('tagName' in node)) return;
39
+ if (node.namespaceURI !== 'http://www.w3.org/1999/xhtml' || ['script', 'iframe', 'object', 'embed', 'base', 'meta', 'link', 'style', 'template'].includes(node.tagName)) {
40
+ report('HESH-HTML-002', `Unsupported active/foreign template element <${node.tagName}>`, at, 'Use semantic HTML and declared CSS/JS modules.'); return;
41
+ }
42
+ const id = `node${serial++}`;
43
+ lines.push(`const ${id} = p.element(${parent}, ${JSON.stringify(node.tagName)});`);
44
+ const attributes = Object.fromEntries(node.attrs.map(attr => [attr.name, attr.value]));
45
+ const attrAt = (name: string) => node.sourceCodeLocation?.attrs?.[name]?.startOffset ?? at;
46
+ for (const attr of node.attrs) {
47
+ if (attr.name.startsWith('h-')) continue;
48
+ if (/^on/i.test(attr.name) || ['srcdoc', 'innerhtml', 'outerhtml'].includes(attr.name)) { report('HESH-HTML-003', `Unsafe attribute ${attr.name}`, attrAt(attr.name), 'Use h-action and safe DOM bindings.'); continue; }
49
+ const wholeBinding = attr.value.startsWith('{') && attr.value.endsWith('}') && !attr.value.slice(1, -1).includes('{') && !attr.value.slice(1, -1).includes('}');
50
+ lines.push(`p.attribute(${id}, ${JSON.stringify(attr.name)}, () => ${wholeBinding ? path(attr.value.slice(1, -1), attrAt(attr.name) + 1) : interpolation(attr.value, attrAt(attr.name))});`);
51
+ }
52
+ for (const name of Object.keys(attributes)) if (name.startsWith('h-') && !['h-action', 'h-bind', 'h-value', 'h-destination'].includes(name) && !name.startsWith('h-input-')) report('HESH-HTML-004', `Unsupported directive ${name}`, attrAt(name), 'Use supported directives; keyed lists/scoped templates are a later milestone.');
53
+ if (attributes['h-bind'] !== undefined) {
54
+ if (node.childNodes.length) report('HESH-BIND-003', 'h-bind cannot also contain children', at, 'Remove the children or use text interpolation.');
55
+ lines.push(`p.text(${id}, () => ${path(attributes['h-bind'], attrAt('h-bind'))});`);
56
+ } else node.childNodes.forEach(child => visit(child, id));
57
+ if (attributes['h-action'] !== undefined) {
58
+ const name = attributes['h-action']; usedActions.add(name);
59
+ if (!contract.actions.includes(name)) report('HESH-ACTION-001', `Missing action ${name}`, attrAt('h-action'), 'Declare and implement the action.');
60
+ lines.push(`p.bindAction(${id}, state, ${JSON.stringify(name)});`);
61
+ }
62
+ if (attributes['h-value'] !== undefined) {
63
+ const name = attributes['h-value'];
64
+ if (!['input', 'textarea', 'select'].includes(node.tagName) || name.includes('.') || ['checkbox', 'radio', 'file'].includes(attributes.type ?? '') || attributes.multiple !== undefined) report('HESH-BIND-004', 'h-value requires a single-value text control and root writable signal', attrAt('h-value'), 'Use a text input/textarea/single select and a declared signal.');
65
+ path(name, attrAt('h-value'), undefined, true);
66
+ lines.push(`p.bindValue(${id}, state.bindings, ${JSON.stringify(name)});`);
67
+ }
68
+ const destination = attributes['h-destination'];
69
+ const inputs = Object.keys(attributes).filter(name => name.startsWith('h-input-'));
70
+ if (destination !== undefined) {
71
+ linked.add(destination);
72
+ const schema = contract.destinations[destination];
73
+ if (!schema) report('HESH-NAV-001', `Unknown destination ${destination}`, attrAt('h-destination'), 'Declare this destination in the application.');
74
+ if (node.tagName !== 'a' || attributes.href !== undefined) report('HESH-NAV-002', 'h-destination requires an anchor without an explicit href', at, 'Use <a h-destination="Name" ...>.');
75
+ for (const key of Object.keys(schema ?? {})) if (attributes[`h-input-${key.toLowerCase()}`] === undefined) report('HESH-NAV-003', `Missing destination input ${key}`, at, `Add h-input-${key} bound to a declared value.`);
76
+ const entries = inputs.map(attr => {
77
+ const key = Object.keys(schema ?? {}).find(key => key.toLowerCase() === attr.slice(8)) ?? attr.slice(8);
78
+ if (!schema || !Object.hasOwn(schema, key)) report('HESH-NAV-003', `Unknown destination input ${key}`, attrAt(attr), 'Use only inputs declared by the destination.');
79
+ return `${JSON.stringify(key)}: ${path(attributes[attr]!, attrAt(attr), schema?.[key])}`;
80
+ });
81
+ lines.push(`p.attribute(${id}, 'href', () => destinations[${JSON.stringify(destination)}].href({${entries.join(',')}}));`);
82
+ } else if (inputs.length) report('HESH-NAV-003', 'h-input-* requires h-destination', at, 'Declare the destination on this anchor.');
83
+ };
84
+ fragment.childNodes.forEach(node => visit(node, 'parent')); lines.push('}');
85
+ for (const name of contract.bindings) if (!usedBindings.has(name)) report('HESH-BIND-005', `Unused binding ${name}`, 0, 'Remove the declaration if it is not needed by this template.', 'warning');
86
+ for (const name of contract.actions) if (!usedActions.has(name)) report('HESH-ACTION-002', `Unused action ${name}`, 0, 'Remove the declaration if it is not needed by this template.', 'warning');
87
+ return { code: lines.join('\n'), diagnostics, usedBindings: [...usedBindings], usedActions: [...usedActions], destinations: [...linked] };
88
+ }