arcane-os 0.51.2 → 0.52.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.52.0
4
+
5
+ - Add optional `package.documents` to select managed application HTML separately
6
+ from files retained by `include` and `exclude`. The schema-1 projection uses
7
+ top-level `documents`. The entry is always first, `[]` selects the entry only,
8
+ and omission preserves automatic discovery. Secondary paths retain existing
9
+ deterministic ordering while the authored declaration remains unchanged.
10
+ - Use that selection across ordinary inspection, import-map generation,
11
+ development serving, packaging and PWA document processing. Explicitly
12
+ unselected HTML originals retain their content, including iframe-referenced
13
+ documents, while remaining included, served, packaged and offline-eligible
14
+ under the existing resource selection. HTML-import components and shared
15
+ runtime resources retain their existing processing. No dependency, migration,
16
+ default selection change or extra CLI flag is introduced.
17
+
3
18
  ## 0.51.2
4
19
 
5
20
  - Forward caller-selected `temperature` through the focused TWiN Cloud
package/README.md CHANGED
@@ -20,7 +20,7 @@ selected SDK projection or an explicit live SDK source mount. Both profiles
20
20
  share the theme, packaging, event, cancellation,
21
21
  and browser run contracts while retaining their selected application layout.
22
22
 
23
- This checkout defines the `0.51.2` SDK contract. Applications pin one exact npm
23
+ This checkout defines the `0.52.0` SDK contract. Applications pin one exact npm
24
24
  version and lockfile; registry state is deliberately not baked into application
25
25
  artifacts.
26
26
 
@@ -197,19 +197,24 @@ npm exec -- arcane doctor --workspace . --arcane-root "../Arcane OS"
197
197
 
198
198
  ### Overview
199
199
 
200
- Refreshes one selected application's browser runtime map, generates
201
- its standard browser import map, discovers every directly navigable
202
- `.html`/`.htm` document admitted by the selected descriptor's existing
203
- include/exclude rules, and prepares the map artifact plus those managed documents
204
- before writing them in order. A directly navigable entry document declares exactly
205
- one `<meta name="arcane-app-id" content="<selected-id>">`; an unmarked secondary
206
- document may instead carry an active `<base>`.
207
- Wrong or duplicate explicit app identity fails. The renderer then requires one
208
- path-correct base for every selected document. Included HTML files with neither
209
- the identity marker nor an active base are component fragments: they remain
210
- package files and are not rewritten with a document-level import map.
211
- Packaging and development use the same discovery owner. Packaging consumes
212
- the saved managed import maps in directly navigable source pages.
200
+ Refreshes one selected application's browser runtime map, generates its standard
201
+ browser import map, and prepares the map artifact plus the selected managed
202
+ `.html`/`.htm` documents before writing them in order. Optional
203
+ `package.documents` in `arcane-app.json` selects literal app-root-relative HTML
204
+ paths that are already included and not excluded. The entry is always first;
205
+ `[]` selects only the entry. Omitting the field retains automatic selection of
206
+ the entry and included secondary pages with an active `<base>`. An app identity
207
+ meta tag does not select a page. The renderer requires one path-correct active
208
+ base for each selected document.
209
+
210
+ The schema-1 package projection carries this field as `documents`, preserving
211
+ the distinction between omission and `[]`. With explicit selection, other
212
+ included HTML documents keep their exact content, including originals referenced
213
+ through iframes. Selection does not change serving, packaging or offline resource
214
+ membership; component processing and shared runtime-resource traversal retain
215
+ their existing behavior. Inspection, packaging and development use the same
216
+ [document-selection contract](protocols.md#application-document-selection).
217
+ Packaging consumes the saved managed import maps in selected source pages.
213
218
 
214
219
  ```text
215
220
  arcane import-map [--workspace <directory>] [--app <id>]
@@ -279,11 +284,12 @@ Success returns the normal selected-workspace wrapper:
279
284
  }
280
285
  ```
281
286
 
282
- For the direct CLI command, `documentPaths` contains the configured entry first
283
- and every other descriptor-selected HTML/HTM document afterward in
284
- deterministic order. The generated artifact is written first, followed by the
285
- selected documents. A write failure or cancellation can leave earlier writes
286
- in place; the operation does not restore them automatically. After successful
287
+ For the direct CLI command, `documentPaths` contains the configured entry first.
288
+ Other selected paths follow in deterministic lexical order without duplicates.
289
+ The authored `documents` declaration remains as supplied, including `[]`.
290
+ The generated artifact is written first, followed by the selected documents.
291
+ A write failure or cancellation can leave earlier writes in place; the operation
292
+ does not restore them automatically. After successful
287
293
  writes, an observer failure preserves delivery with
288
294
  `eventDelivery.status === 'degraded'` and `ARCANE_EVENT_DELIVERY_FAILED`; it
289
295
  does not roll back complete application content.
@@ -51,16 +51,12 @@ App-scoped managed browser imports have three supported control-plane entrypoint
51
51
  uses `arcane import-map`; Node callers use
52
52
  `executeOperation('import-map', options)` or
53
53
  `createToolchain(defaults).importMap(options)`. These are three routes to the
54
- same app-scoped operation, not three import-map formats. Each route discovers
55
- the selected descriptor's directly navigable admitted `.html`/`.htm` documents
56
- through the package include/exclude rules and writes one canonical map to the
57
- artifact plus every such document. The current marker is exactly one matching
58
- `meta[name="arcane-app-id"]`; an unmarked secondary page with an active `base`
59
- remains admitted under the current secondary-document rule. Wrong or duplicate
60
- explicit identity fails, and the selected page then must pass exact path-relative
61
- base validation.
62
- Included HTML with neither signal is a component fragment and remains a
63
- complete package file.
54
+ same app-scoped operation, not three import-map formats. Each route uses the
55
+ [application document selection](#application-document-selection) shared by
56
+ inspection, development and packaging, and writes one canonical map to the
57
+ artifact plus every selected document. Each selected page retains the existing
58
+ path-relative base requirement. Included component fragments remain complete
59
+ package files and keep their existing component processing.
64
60
  Public lowercase runtime subpaths resolve directly to canonical runtime-module
65
61
  namespaces from both Node package exports and managed browser keys. These
66
62
  include `arcane-os/preference-store`, `arcane-os/speech-playback`,
@@ -98,6 +94,48 @@ materializes the complete required SDK runtime,
98
94
  browser-runtime, and managed-import-map closure inside that app's own artifact.
99
95
  No application polls for SDK changes.
100
96
 
97
+ ## Application document selection
98
+
99
+ An authored schema-2 `arcane-app.json` may select the HTML pages that receive
100
+ application-document processing through `package.documents`:
101
+
102
+ ```json
103
+ {
104
+ "package": {
105
+ "entry": "index.html",
106
+ "include": ["index.html", "pages", "originals", "modules", "css"],
107
+ "documents": ["index.html", "pages/viewer.html"]
108
+ }
109
+ }
110
+ ```
111
+
112
+ This is a package excerpt. Each `documents` member is a literal app-root-relative
113
+ `.html` or `.htm` file path, not a directory or glob. Every selected document
114
+ must already be present in the app's included, non-excluded files. The entry is
115
+ always selected first, followed by the other selected paths in deterministic
116
+ lexical order without duplicates. An empty list selects only the entry. The
117
+ authored `documents` declaration remains as supplied, including `[]`.
118
+
119
+ Omitting `documents` preserves automatic discovery: the SDK selects the entry
120
+ and included secondary HTML pages with an active `<base>`. A
121
+ `meta[name="arcane-app-id"]` marker does not select a page. Selected pages retain
122
+ the existing requirement for one path-correct active base.
123
+
124
+ The schema-1 `arcane-package.json` field is `documents` at the top level.
125
+ Descriptor projection and synthesis preserve an omitted property as omitted
126
+ and an authored empty list as `[]`. Package-only applications may author that
127
+ schema-1 field directly. Ordinary inspect, import-map, dev and package operations
128
+ use the same declaration; there is no document-selection CLI flag.
129
+
130
+ Document selection controls application-document scanning, base handling,
131
+ managed import maps and PWA injection. It does not change `include`/`exclude`,
132
+ serving, package membership or offline resource selection. With an explicit
133
+ list, other included HTML documents retain their exact content, even when a
134
+ selected page references them through an iframe. They remain available as
135
+ included resources without receiving application-document processing.
136
+ Component processing and shared runtime-resource traversal retain their existing
137
+ behavior. Offline inclusion still follows the app's PWA resource selection.
138
+
101
139
  ## App-selected package output
102
140
 
103
141
  An app may set `"outputDirectory": "ai"` in its schema-1
@@ -58,6 +58,27 @@ previous source identity while its start URL and scope move to the root. An
58
58
  authored `manifest.id` remains authoritative. Packaged default identity remains
59
59
  `./`. App identity and saved application data remain unchanged.
60
60
 
61
+ ### Application pages and preserved HTML resources
62
+
63
+ Optional `package.documents` in `arcane-app.json` selects the app-root-relative
64
+ `.html`/`.htm` pages that receive managed import maps and PWA links/bootstrap.
65
+ Its schema-1 projection is `arcane-package.json`'s top-level `documents`.
66
+ For example, `"documents": ["index.html"]` selects the entry while other
67
+ included HTML documents retain their exact content. The entry is always selected;
68
+ `[]` also selects only the entry. Omitting the property preserves automatic
69
+ discovery of the entry and included secondary pages with an active `<base>`.
70
+ Selected pages retain the existing path-correct base requirement.
71
+
72
+ This declaration does not add or remove resources from `package.include`,
73
+ `package.exclude`, serving, packaging or the offline inventory. Included
74
+ originals remain available and can be cached under the normal offline selection,
75
+ including when a selected page displays them in an iframe. With an explicit
76
+ list, those unselected documents are not scanned as application pages or given
77
+ managed import maps, base handling or PWA injection. HTML-import components and
78
+ shared runtime-resource traversal keep their existing processing. See
79
+ [application document selection](protocols.md#application-document-selection)
80
+ for ordering and the complete declaration contract.
81
+
61
82
  ### Root generated output
62
83
 
63
84
  For a standalone application, `appsRoot: "."` places the app at its repository
@@ -136,16 +157,18 @@ their app and SDK versions match. It is not a content measurement.
136
157
 
137
158
  The package owns its selected inventory once, after any app adapter finishes.
138
159
  It follows actual resource references to include meaningful query variants.
139
- Generated application pages receive a manifest link and an `async` module
160
+ Selected application pages receive a manifest link and an `async` module
140
161
  marked `data-arcane-pwa`. Existing application scripts retain their order.
141
162
  PWA registration does not wait for models, storage, preferences or page rendering.
142
163
  The same bootstrap starts one initially hidden `pwa-install.html` component with
143
164
  the generated manifest's app name. Component loading and worker registration
144
165
  proceed independently.
145
166
 
146
- All SDK browser-resource delivery removes only SDK-owned `arcaneVersion` fields
147
- from actual local resource references, including the managed import map. Authored
148
- fields such as `v`, encoded and repeated fields, empty query segments, fragments,
167
+ SDK processing of selected application pages, components and runtime resources
168
+ removes only SDK-owned `arcaneVersion` fields from actual local resource references,
169
+ including the managed import map. Explicitly unselected HTML documents keep their
170
+ exact content. Authored fields such as `v`, encoded and repeated fields, empty
171
+ query segments, fragments,
149
172
  source spelling and unrelated payloads are preserved. The offline
150
173
  manifest carries release information. Non-PWA and native packaging use the same
151
174
  [clean local resource URLs](asset-versioning.md). Native packaging does not
@@ -179,9 +202,9 @@ If `package.pwa.offline.include` is nonempty, the resource must also
179
202
  match that offline selection and must not match `offline.exclude`. Adding a
180
203
  path only to the offline selection does not add it to the app's resources.
181
204
  The running source server reads changed app descriptors before the next app,
182
- root-navigation or generated-PWA request. Include/exclude rules, the entry and
183
- PWA settings share the current selection; no restart or package projection
184
- write is needed. Edits to selected source files are picked up by the next due
205
+ root-navigation or generated-PWA request. Include/exclude rules, the entry,
206
+ document selection and PWA settings share the current selection; no restart or
207
+ package projection write is needed. Edits to selected source files are picked up by the next due
185
208
  page-load check while the server remains running.
186
209
 
187
210
  For an enabled application, `arcane dev` serves the generated PWA files at the
@@ -779,12 +779,22 @@ async inspectApp({workspaceRoot, appId, signal}={})
779
779
  ```
780
780
 
781
781
  Import it from `arcane-os` or `arcane-os/packager`. The result includes the
782
- configured `entry`, `include`, and `exclude` values plus `browserDocuments`, the
783
- entry-first ordered `.html`/`.htm` paths selected by the same package traversal
784
- used by import-map refresh and packaging. Current pages declare exactly one
785
- matching `meta[name="arcane-app-id"]`; unmarked pages with an active `base`
786
- remain selected for patch compatibility. Included HTML with neither signal is
787
- retained as a package fragment rather than rewritten as a document.
782
+ configured `entry`, `include`, and `exclude` values, the optional `documents`
783
+ declaration when authored, and `browserDocuments`, the entry-first ordered
784
+ `.html`/`.htm` paths shared by import-map refresh, development and packaging.
785
+ `package.documents` in an authored descriptor, or `documents` in a schema-1
786
+ package, selects literal app-root-relative paths already included and not
787
+ excluded. Other selected paths follow the entry in deterministic lexical order
788
+ without duplicates. The authored `documents` declaration remains as supplied,
789
+ including `[]`, which selects only the entry. Omitting the property retains
790
+ automatic discovery of the entry and included secondary pages with an active `<base>`. App identity
791
+ meta tags do not select documents.
792
+
793
+ Selected pages retain their existing base requirements. Explicit selection
794
+ leaves other included HTML documents unchanged, including iframe-referenced
795
+ originals, while preserving package, serving and offline resource membership.
796
+ Component processing and shared runtime-resource traversal retain their existing
797
+ behavior. See [application document selection](protocols.md#application-document-selection).
788
798
 
789
799
  `entry` and each app browser document's `path` stay relative to the app
790
800
  directory. Each browser document also reports `packagePath`, the emitted path
@@ -2111,6 +2121,11 @@ async function useprojectNativeDescriptor(...arguments_) {
2111
2121
 
2112
2122
  Projects a schema-2 descriptor into the compatible schema-1 application package contract.
2113
2123
 
2124
+ Optional `package.documents` becomes top-level `documents`. Omission stays
2125
+ omitted and an authored empty list stays `[]`, so projection preserves automatic
2126
+ discovery versus entry-only selection. See
2127
+ [application document selection](protocols.md#application-document-selection).
2128
+
2114
2129
  ### Signature and result
2115
2130
 
2116
2131
  ```text
@@ -4277,9 +4292,9 @@ async function usedevelopApplication(...arguments_) {
4277
4292
 
4278
4293
  Dispatches one named headless SDK operation with normalized acceptance, events, cancellation, and failure.
4279
4294
 
4280
- The exact command `'import-map'` dispatches one app-scoped refresh
4281
- across directly navigable `.html`/`.htm` documents selected by the descriptor's
4282
- existing include/exclude rules, excluding component fragments, and returns
4295
+ The exact command `'import-map'` dispatches one app-scoped refresh across the
4296
+ `.html`/`.htm` documents selected by
4297
+ [`package.documents` or automatic discovery](protocols.md#application-document-selection), and returns
4283
4298
  `{workspaceRoot, workspaceMode, appId,
4284
4299
  importMap}`. The `importMap` value reports the generated imports and complete
4285
4300
  ordered `documentPaths`. This route mutates the map artifact and selected managed HTML
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.51.2",
3
+ "version": "0.52.0",
4
4
  "description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -38,6 +38,11 @@
38
38
  "required": ["entry", "strategy", "include", "exclude", "shared"],
39
39
  "properties": {
40
40
  "entry": { "$ref": "#/$defs/relativePath" },
41
+ "documents": {
42
+ "type": "array",
43
+ "description": "Application-relative HTML/HTM pages managed by import-map and PWA processing. Entry is always first; an empty list selects entry only. Omission preserves automatic discovery. File inclusion and serving remain controlled by include/exclude.",
44
+ "items": { "allOf": [{ "$ref": "#/$defs/relativePath" }, { "pattern": "\\.[Hh][Tt][Mm][Ll]?$" }] }
45
+ },
41
46
  "strategy": { "enum": ["static", "adapter"] },
42
47
  "localAIModelPolicy": { "$ref": "#/$defs/localAIModelPolicy" },
43
48
  "pwa": { "$ref": "#/$defs/pwa" },
@@ -34,6 +34,16 @@
34
34
  "entry": {
35
35
  "$ref": "#/$defs/relativePath"
36
36
  },
37
+ "documents": {
38
+ "type": "array",
39
+ "description": "Application-relative HTML/HTM pages managed by import-map and PWA processing. Entry is always first; an empty list selects entry only. Omission preserves automatic discovery. File inclusion and serving remain controlled by include/exclude.",
40
+ "items": {
41
+ "allOf": [
42
+ { "$ref": "#/$defs/relativePath" },
43
+ { "pattern": "\\.[Hh][Tt][Mm][Ll]?$" }
44
+ ]
45
+ }
46
+ },
37
47
  "outputDirectory": {
38
48
  "$ref": "#/$defs/relativePath",
39
49
  "description": "Final package directory relative to the workspace, with no appended app ID. Omission preserves dist/<id>. This directory is replaced when packaging succeeds; source layout and app identity remain unchanged."
@@ -12,6 +12,7 @@ import {
12
12
  } from './constants.mjs';
13
13
  import {
14
14
  normalizeRelativePath,
15
+ normalizeAppDocuments,
15
16
  parseSemver,
16
17
  validateAppConfig as validateAppPackageConfig
17
18
  } from './packager/core.mjs';
@@ -110,9 +111,10 @@ function validateLocalAIModelPolicy(value,label){
110
111
 
111
112
  function validatePackage(value,appId){
112
113
  assertOnlyKeys(value,new Set([
113
- 'entry','strategy','include','exclude','shared','adapter','localAIModelPolicy','pwa'
114
+ 'entry','strategy','include','exclude','shared','adapter','localAIModelPolicy','pwa','documents'
114
115
  ]),'descriptor.package');
115
116
  const entry=normalizeRelativePath(value.entry,'descriptor.package.entry');
117
+ const documents=normalizeAppDocuments(value.documents,'descriptor.package.documents');
116
118
  if(!['static','adapter'].includes(value.strategy))fail('descriptor.package.strategy must be static or adapter.');
117
119
  const include=relativePaths(value.include,'descriptor.package.include',{required:true});
118
120
  const exclude=relativePaths(value.exclude??[],'descriptor.package.exclude');
@@ -134,6 +136,7 @@ function validatePackage(value,appId){
134
136
  }
135
137
  return completeValue({
136
138
  entry,
139
+ ...(documents===undefined?{}:{documents}),
137
140
  strategy:value.strategy,
138
141
  include,
139
142
  exclude,
@@ -317,6 +320,7 @@ export function projectPackageManifest(descriptor){
317
320
  displayName:value.displayName,
318
321
  version:value.version,
319
322
  entry:value.package.entry,
323
+ ...(value.package.documents===undefined?{}:{documents:[...value.package.documents]}),
320
324
  strategy:value.package.strategy,
321
325
  ...(hasAuthoredSecurity?{security:{
322
326
  connectOrigins:[...value.security.connectOrigins],
@@ -381,6 +385,7 @@ function synthesizedDescriptor(packageManifest,nativeDescriptor){
381
385
  publisher:{id:'publisher-undeclared',name:'Publisher not declared'},
382
386
  package:{
383
387
  entry:packageManifest.entry,
388
+ ...(packageManifest.documents===undefined?{}:{documents:[...packageManifest.documents]}),
384
389
  strategy:packageManifest.strategy,
385
390
  ...(packageManifest.localAIModelPolicy?{localAIModelPolicy:packageManifest.localAIModelPolicy}:{}),
386
391
  ...(packageManifest.pwa===undefined?{}:{pwa:packageManifest.pwa}),
@@ -324,7 +324,8 @@ async function serveGeneratedRepresentation(fileServer, request, response, {
324
324
  }
325
325
 
326
326
  async function serveSourceFile(fileServer, request, response, opened, {
327
- assetVersion, revalidate = false, onReference, pwaEntry = false, lastModified
327
+ assetVersion, revalidate = false, onReference, pwaEntry = false, lastModified,
328
+ representationTracked = false
328
329
  } = {}) {
329
330
  const extension = path.extname(opened.candidate).toLowerCase();
330
331
  const rewrite = assetVersion !== undefined && /\.(?:m?js|html?|css|json)$/iu.test(extension);
@@ -336,9 +337,27 @@ async function serveSourceFile(fileServer, request, response, opened, {
336
337
  response.writeHead = function writeDevelopmentHeaders(...arguments_) {
337
338
  this.removeHeader('ETag');
338
339
  this.removeHeader('X-Content-Type-Options');
340
+ if (representationTracked) {
341
+ if (lastModified) this.setHeader('Last-Modified', lastModified.toUTCString());
342
+ else this.removeHeader('Last-Modified');
343
+ }
339
344
  return writeHead.apply(this, arguments_);
340
345
  };
341
- await fileServer.serveFile(opened.candidate, request, response);
346
+ const modifiedSince = request.headers['if-modified-since'];
347
+ const range = request.headers.range;
348
+ if (representationTracked) {
349
+ const currentTime = lastModified ? Date.parse(lastModified.toUTCString()) : NaN;
350
+ if (!(Date.parse(modifiedSince) >= currentTime)) delete request.headers['if-modified-since'];
351
+ if (request.headers['if-range'] && !(Date.parse(request.headers['if-range']) >= currentTime)) {
352
+ delete request.headers.range;
353
+ }
354
+ }
355
+ try {
356
+ await fileServer.serveFile(opened.candidate, request, response);
357
+ } finally {
358
+ if (modifiedSince !== undefined) request.headers['if-modified-since'] = modifiedSince;
359
+ if (range !== undefined) request.headers.range = range;
360
+ }
342
361
  return;
343
362
  }
344
363
  await serveGeneratedRepresentation(
@@ -514,6 +533,10 @@ function applicationSourcePath(config,appId,relative='') {
514
533
  return `/${root?`${root}/`:''}${relative}`;
515
534
  }
516
535
 
536
+ function selectedApplicationDocument(app,relative){
537
+ return app.documents===undefined||relative===app.entry||app.documents.includes(relative);
538
+ }
539
+
517
540
  async function packagedRoutes(releaseRoot){
518
541
  if(!is.string(releaseRoot)||!releaseRoot.trim())fail('releaseRoot is required in packaged mode.','ARCANE_USAGE');
519
542
  const requested=path.resolve(releaseRoot);
@@ -548,7 +571,7 @@ async function packagedRoutes(releaseRoot){
548
571
  };
549
572
  }
550
573
 
551
- async function sourcePwaAssets(routeSet, mappings, signal, resourceUrls, resourcePaths) {
574
+ async function sourcePwaAssets(routeSet, mappings, signal, resourceUrls, resourcePaths, runtimeDocuments = new Set()) {
552
575
  const records = new Map();
553
576
  const resources = new Map();
554
577
  for (const mapping of mappings) {
@@ -590,7 +613,7 @@ async function sourcePwaAssets(routeSet, mappings, signal, resourceUrls, resourc
590
613
  const pending = [{url: entryUrl, documentUrl: entryUrl}];
591
614
  const visited = new Set();
592
615
  const referencesByPath = new Map();
593
- for (const selected of routeSet.app.include) {
616
+ for (const selected of routeSet.app.documents ?? routeSet.app.include) {
594
617
  if (!/\.html?$/iu.test(selected)) continue;
595
618
  const url = new URL(
596
619
  applicationSourcePath(routeSet.config,routeSet.appId,selected.split('/').map(encodeURIComponent).join('/')),
@@ -609,6 +632,9 @@ async function sourcePwaAssets(routeSet, mappings, signal, resourceUrls, resourc
609
632
  if (!/\.(?:m?js|html?|css)$/iu.test(pathname) && !managedMap) continue;
610
633
  visited.add(context);
611
634
  resourcePaths.add(decodeURIComponent(pathname));
635
+ if (record.mapping.kind !== 'app' && /\.html?$/iu.test(pathname)) {
636
+ runtimeDocuments.add(decodeURIComponent(pathname));
637
+ }
612
638
  let references = referencesByPath.get(pathname);
613
639
  if (!references) {
614
640
  const opened = await openSafeFile(record.mapping.root, record.relative);
@@ -654,6 +680,9 @@ async function sourcePwaAssets(routeSet, mappings, signal, resourceUrls, resourc
654
680
  }
655
681
  if (target.origin !== origin || !resources.has(target.pathname)) continue;
656
682
  resourceUrls.add(`${target.pathname}${target.search}`);
683
+ const targetRecord = resources.get(target.pathname);
684
+ if (kind === 'document' && targetRecord.mapping.kind === 'app'
685
+ && !selectedApplicationDocument(routeSet.app, targetRecord.relative.join('/'))) continue;
657
686
  const traversable = kind !== 'fetch'
658
687
  && (kind !== 'asset' || /\.css(?:[?#]|$)/iu.test(url));
659
688
  if (traversable) {
@@ -942,21 +971,33 @@ async function startOwnedDevServer({
942
971
  );
943
972
  return managed;
944
973
  }
945
- function sourceRepresentationModifiedAt(opened, version, pwaEntry) {
974
+ function sourceRepresentation(opened, version, pwaEntry, rewritten) {
946
975
  const modifiedAt = opened.modifiedAt.getTime();
947
976
  const previous = representationMetadata.get(opened.candidate);
948
977
  if (previous?.modifiedAt === modifiedAt
949
- && previous.version === version && previous.pwaEntry === pwaEntry) {
950
- return previous.lastModified;
978
+ && previous.version === version && previous.pwaEntry === pwaEntry
979
+ && previous.rewritten === rewritten) {
980
+ if (!previous.lastModified && Date.now() >= previous.ambiguousThrough + 1000) {
981
+ previous.lastModified = new Date();
982
+ }
983
+ return previous;
951
984
  }
952
985
  const lastModified = new Date(
953
986
  Math.max(modifiedAt, generatorModifiedAt, assetVersionModifiedAt, previous ? Date.now() : 0)
954
987
  );
955
- representationMetadata.set(
956
- opened.candidate,
957
- {modifiedAt, version, pwaEntry, lastModified}
958
- );
959
- return lastModified;
988
+ const previousTime = previous?.lastModified
989
+ ? Date.parse(previous.lastModified.toUTCString()) : previous?.ambiguousThrough;
990
+ // HTTP dates cannot distinguish two representations within one second.
991
+ // Omit that ambiguous validator until a later request can date it honestly.
992
+ const ambiguous = previousTime !== undefined
993
+ && Date.parse(lastModified.toUTCString()) <= previousTime;
994
+ const record = {
995
+ modifiedAt, version, pwaEntry, rewritten,
996
+ lastModified: ambiguous ? undefined : lastModified,
997
+ ambiguousThrough: ambiguous ? previousTime : undefined
998
+ };
999
+ representationMetadata.set(opened.candidate, record);
1000
+ return record;
960
1001
  }
961
1002
  for(const mapping of mappings){
962
1003
  const info=await lstat(mapping.root);
@@ -1089,7 +1130,7 @@ async function startOwnedDevServer({
1089
1130
  const generatedAt = new Date();
1090
1131
  state.inventoryTask = Promise.all(
1091
1132
  [
1092
- sourcePwaAssets(selectedRoutes, selectedRoutes.mappings, signal, pwaResourceUrls, resourcePaths),
1133
+ sourcePwaAssets(selectedRoutes, selectedRoutes.mappings, signal, pwaResourceUrls, resourcePaths, runtimeDocuments),
1093
1134
  selectedAssetVersion().then(
1094
1135
  function rememberCurrentInventoryVersion(version) {
1095
1136
  if (selectedRoutes === currentSourceRoutes) rememberAssetVersion(version);
@@ -1124,6 +1165,66 @@ async function startOwnedDevServer({
1124
1165
  // Remember actual resource edges as their owners are served, not every
1125
1166
  // HTML/JS/CSS file in an application's document or attachment corpus.
1126
1167
  const resourcePaths=new Set([routeSet.startPath]);
1168
+ const runtimeDocuments=new Set();
1169
+ const componentReferences=new Map();
1170
+ async function isCurrentComponent(targetPath, selectedRoutes) {
1171
+ // Retain parsed edges, not lifetime classifications. Only current page
1172
+ // roots and observed runtime documents can make an original a component.
1173
+ const pending = [selectedRoutes.startPath, ...runtimeDocuments];
1174
+ for (const document of selectedRoutes.app.documents) {
1175
+ pending.push(applicationSourcePath(selectedRoutes.config, selectedRoutes.appId, document));
1176
+ }
1177
+ const visited = new Set();
1178
+ for (const ownerPath of pending) {
1179
+ throwIfAborted(signal);
1180
+ if (visited.has(ownerPath)) continue;
1181
+ visited.add(ownerPath);
1182
+ const target = parseRequestTarget(ownerPath.split('/').map(encodeURIComponent).join('/'));
1183
+ if (!target) continue;
1184
+ const mapping = selectedRoutes.mappings.find(function componentOwnerMapping(route) {
1185
+ return route.prefix.every(function componentOwnerSegment(segment, index) {
1186
+ return target.segments[index] === segment;
1187
+ });
1188
+ });
1189
+ if (!mapping) continue;
1190
+ const relative = target.segments.slice(mapping.prefix.length);
1191
+ if (!relative.length || (mapping.allow && !mapping.allow(relative))) continue;
1192
+ if (!/\.html?$/iu.test(ownerPath)) continue;
1193
+ const opened = await openSafeFile(mapping.root, relative, {readContent: false});
1194
+ if (!opened) continue;
1195
+ const modifiedAt = opened.modifiedAt.getTime();
1196
+ let record = componentReferences.get(opened.candidate);
1197
+ if (record?.modifiedAt !== modifiedAt) {
1198
+ const references = [];
1199
+ rewriteAssetReferences(await readFile(opened.candidate, 'utf8'), {
1200
+ filePath: opened.candidate,
1201
+ version: null,
1202
+ onReference: function currentComponentReference(reference) {
1203
+ if (reference.kind === 'component') references.push(reference);
1204
+ }
1205
+ });
1206
+ record = {modifiedAt, references};
1207
+ componentReferences.set(opened.candidate, record);
1208
+ }
1209
+ const ownerUrl = new URL(target.pathname, 'http://arcane.invalid');
1210
+ for (const reference of record.references) {
1211
+ let component;
1212
+ try {
1213
+ component = resolveAssetReference(reference, {ownerUrl});
1214
+ } catch {
1215
+ // Non-URL values retain their authored behavior.
1216
+ continue;
1217
+ }
1218
+ if (component.origin !== ownerUrl.origin) continue;
1219
+ const componentTarget = parseRequestTarget(component.pathname);
1220
+ if (!componentTarget) continue;
1221
+ const componentPath = componentTarget.path;
1222
+ if (componentPath === targetPath) return true;
1223
+ pending.push(componentPath);
1224
+ }
1225
+ }
1226
+ return false;
1227
+ }
1127
1228
  const requestTasks=new Set();
1128
1229
  const runFileWork=createFileWorkLimiter();
1129
1230
  async function serveDevelopmentRequest(request, response) {
@@ -1208,21 +1309,31 @@ async function startOwnedDevServer({
1208
1309
  }
1209
1310
  const extension=path.extname(opened.candidate).toLowerCase();
1210
1311
  const html=extension==='.html'||extension==='.htm';
1312
+ const explicitDocuments = mode === 'source' && mapping.kind === 'app'
1313
+ && selectedRoutes.app.documents !== undefined;
1314
+ const explicitDocument = explicitDocuments && html
1315
+ && selectedApplicationDocument(selectedRoutes.app, relative.join('/'));
1211
1316
  const selectedPwaDocument = mode === 'source' && pwaEnabled && html
1212
1317
  && mapping.kind === 'app'
1213
- && selectedRoutes.app.include.includes(relative.join('/'));
1214
- const selectedDocument = target.path === selectedRoutes.startPath || selectedPwaDocument;
1215
- const managedDocument = !selectedDocument && html && await isManagedDocument(opened);
1318
+ && !explicitDocuments && selectedRoutes.app.include.includes(relative.join('/'));
1319
+ const selectedDocument = target.path === selectedRoutes.startPath || explicitDocument || selectedPwaDocument;
1320
+ const managedDocument = !selectedDocument && !explicitDocuments && html && await isManagedDocument(opened);
1216
1321
  const entryDocument = selectedDocument || managedDocument;
1217
1322
  const managedMap=path.basename(opened.candidate)==='arcane.importmap.json';
1218
1323
  // A live server can span an SDK upgrade. Refresh the small
1219
1324
  // version record on navigation, not on each resource request.
1220
1325
  if(entryDocument||managedMap)rememberAssetVersion(await selectedAssetVersion());
1221
1326
  const runtimeResource=mapping.kind!=='app';
1327
+ if (runtimeResource && html) runtimeDocuments.add(target.path);
1222
1328
  const browserResource=['script','style','worker','sharedworker','serviceworker']
1223
1329
  .includes(request.headers['sec-fetch-dest']);
1224
- const rewrite=runtimeResource||entryDocument||browserResource||managedMap
1225
- ||resourcePaths.has(target.path);
1330
+ // Selecting application pages does not turn retained originals
1331
+ // into resources. HTML components remain owned by html-import.
1332
+ const componentDocument=explicitDocuments&&html&&!selectedDocument
1333
+ &&await isCurrentComponent(target.path, selectedRoutes);
1334
+ const retainedDocument=explicitDocuments&&html&&!selectedDocument&&!componentDocument;
1335
+ const rewrite=!retainedDocument&&(runtimeResource||entryDocument||browserResource||managedMap
1336
+ ||componentDocument||resourcePaths.has(target.path));
1226
1337
  const onReference = function observeServedResource(reference) {
1227
1338
  const {url,kind,baseKind}=reference;
1228
1339
  if (pwaEnabled && kind !== 'fetch'
@@ -1247,7 +1358,13 @@ async function startOwnedDevServer({
1247
1358
  const resource=resolveAssetReference(reference,{ownerUrl:documentUrl});
1248
1359
  if(resource.origin===documentUrl.origin
1249
1360
  &&/\.(?:m?js|html?|css)$/iu.test(resource.pathname)){
1250
- resourcePaths.add(decodeURIComponent(resource.pathname));
1361
+ const resourcePath=decodeURIComponent(resource.pathname);
1362
+ const appPrefix=applicationSourcePath(selectedRoutes.config,selectedRoutes.appId);
1363
+ const appRelative=resourcePath.startsWith(appPrefix)?resourcePath.slice(appPrefix.length):null;
1364
+ if(kind==='document'&&mode==='source'&&appRelative!==null
1365
+ &&sourcePathAllowed(appRelative.split('/'),selectedRoutes.app)
1366
+ &&!selectedApplicationDocument(selectedRoutes.app,appRelative))return;
1367
+ resourcePaths.add(resourcePath);
1251
1368
  }
1252
1369
  }catch{ /* Non-URL values remain under their existing owner. */ }
1253
1370
  };
@@ -1255,6 +1372,8 @@ async function startOwnedDevServer({
1255
1372
  const pwaEntry = mode === 'source' && pwaEnabled && entryDocument;
1256
1373
  const transformed = pwaEntry || (selectedVersion !== undefined
1257
1374
  && /\.(?:m?js|html?|css|json)$/iu.test(extension));
1375
+ const representation = transformed || retainedDocument
1376
+ ? sourceRepresentation(opened, assetVersion, pwaEntry, transformed) : undefined;
1258
1377
  await serveSourceFile(
1259
1378
  fileServer,
1260
1379
  request,
@@ -1265,9 +1384,8 @@ async function startOwnedDevServer({
1265
1384
  revalidate: pwaEnabled || entryDocument || managedMap,
1266
1385
  pwaEntry,
1267
1386
  onReference,
1268
- lastModified: transformed
1269
- ? sourceRepresentationModifiedAt(opened, assetVersion, pwaEntry)
1270
- : undefined
1387
+ lastModified: representation?.lastModified,
1388
+ representationTracked: representation !== undefined
1271
1389
  }
1272
1390
  );
1273
1391
  });
@@ -166,6 +166,16 @@ function validatePathList(value,label,{required=false,allowRoot=false}={}){
166
166
  return normalized;
167
167
  }
168
168
 
169
+ export function normalizeAppDocuments(value,label='documents'){
170
+ if(value===undefined)return undefined;
171
+ if(!is.array(value))fail(`${label} must be an array of application-relative HTML paths.`);
172
+ return value.map(function normalizeDocumentPath(entry,index){
173
+ const document=normalizeRelativePath(entry,`${label}[${index}]`);
174
+ if(!/\.html?$/iu.test(document))fail(`${label}[${index}] must name an HTML or HTM file.`);
175
+ return document;
176
+ });
177
+ }
178
+
169
179
  function isAlwaysForbidden(relative){
170
180
  return relative.split('/').some(segment=>{
171
181
  const key=pathKey(segment);
@@ -308,7 +318,7 @@ function normalizeOptionalRecord(value,label){
308
318
  export function validateAppConfig(value,appId,rootConfig,configPath=path.posix.join(appRelativeRoot(rootConfig,appId),APP_CONFIG_NAME)){
309
319
  assertOnlyKeys(value,new Set([
310
320
  'schemaVersion','id','displayName','version','entry','strategy','security',
311
- 'localAIModelPolicy','include','exclude','shared','adapter','pwa','outputDirectory'
321
+ 'localAIModelPolicy','include','exclude','shared','adapter','pwa','outputDirectory','documents'
312
322
  ]),`${appId}/${APP_CONFIG_NAME}`);
313
323
  if(value.schemaVersion!==1)fail(`${appId}/${APP_CONFIG_NAME}.schemaVersion must be 1.`);
314
324
  if(!is.string(value.id)||value.id!==appId||!APP_ID_PATTERN.test(value.id)){
@@ -317,6 +327,7 @@ export function validateAppConfig(value,appId,rootConfig,configPath=path.posix.j
317
327
  const displayName=assertPresentationText(value.displayName,`${appId}/${APP_CONFIG_NAME}.displayName`);
318
328
  parseSemver(value.version);
319
329
  const entry=normalizeRelativePath(value.entry,`${appId}/${APP_CONFIG_NAME}.entry`);
330
+ const documents=normalizeAppDocuments(value.documents,`${appId}/${APP_CONFIG_NAME}.documents`);
320
331
  const outputDirectory=value.outputDirectory===undefined?undefined:normalizeRelativePath(
321
332
  value.outputDirectory,`${appId}/${APP_CONFIG_NAME}.outputDirectory`
322
333
  );
@@ -355,6 +366,7 @@ export function validateAppConfig(value,appId,rootConfig,configPath=path.posix.j
355
366
  displayName,
356
367
  version:value.version,
357
368
  entry,
369
+ ...(documents===undefined?{}:{documents}),
358
370
  ...(outputDirectory===undefined?{}:{outputDirectory}),
359
371
  strategy:value.strategy,
360
372
  ...(value.pwa===undefined?{}:{pwa:normalizePwaConfig(value.pwa)}),
@@ -603,7 +615,23 @@ async function collectPackageRecords(context,{signal}={}){
603
615
  return records;
604
616
  }
605
617
 
606
- async function browserDocuments(records,entry){
618
+ async function browserDocuments(records,entry,selected){
619
+ if(selected!==undefined){
620
+ const appRecords=new Map(records.filter(function applicationRecord(record){
621
+ return record.appRelativePath!==undefined;
622
+ }).map(function applicationRecordPath(record){
623
+ return [record.appRelativePath,record];
624
+ }));
625
+ const documentPaths=[entry,...[...new Set(selected)].filter(function secondaryDocument(document){
626
+ return document!==entry;
627
+ }).sort(compareText)];
628
+ return Promise.all(documentPaths.map(async function inspectSelectedDocument(documentPath){
629
+ const record=appRecords.get(documentPath);
630
+ if(!record)fail(`Application document is missing from the selected files: ${documentPath}.`);
631
+ const inspected=inspectImportMapHtml(await readFile(record.source,'utf8'),{documentPath});
632
+ return {path:documentPath,packagePath:record.destination,...copyJson(inspected)};
633
+ }));
634
+ }
607
635
  let entryDocument=null;
608
636
  const documents=[];
609
637
  for(const record of records){
@@ -638,12 +666,13 @@ async function optionalDescriptor(context){
638
666
 
639
667
  async function inspectContext(context,{signal,records}={}){
640
668
  const selectedRecords=records??await collectPackageRecords(context,{signal});
641
- const documents=await browserDocuments(selectedRecords,context.config.entry);
669
+ const documents=await browserDocuments(selectedRecords,context.config.entry,context.config.documents);
642
670
  return {
643
671
  appId:context.appId,
644
672
  displayName:context.config.displayName,
645
673
  version:context.config.version,
646
674
  entry:context.config.entry,
675
+ ...(context.config.documents===undefined?{}:{documents:[...context.config.documents]}),
647
676
  ...(context.config.outputDirectory===undefined?{}:{outputDirectory:context.config.outputDirectory}),
648
677
  strategy:context.config.strategy,
649
678
  ...(context.config.pwa===undefined?{}:{pwa:copyJson(context.config.pwa)}),
@@ -888,9 +917,12 @@ async function packageWithContext(context,options={}){
888
917
  const offlineReferences=new Set();
889
918
  const entryUrl=new URL(packageResourceUrl(entryPath),'http://arcane.invalid/');
890
919
  const pwaDocumentSources=new Map();
920
+ const explicitDocumentPaths=context.config.documents===undefined?null:new Set(
921
+ inspected.browserDocuments.map(function selectedDocumentPath(document){return document.packagePath;})
922
+ );
891
923
  if(pwaEnabled){
892
- const documentPaths=new Set([entryPath]);
893
- for(const selected of context.config.include){
924
+ const documentPaths=new Set(explicitDocumentPaths??[entryPath]);
925
+ for(const selected of explicitDocumentPaths===null?context.config.include:[]){
894
926
  const selectedPath=appPackagePath(context,selected);
895
927
  if(/\.html?$/iu.test(selected)&&inventory.has(selectedPath))documentPaths.add(selectedPath);
896
928
  }
@@ -925,6 +957,9 @@ async function packageWithContext(context,options={}){
925
957
  ?new URL(entryDocument.bases[0].href,entryUrl):entryUrl;
926
958
  const pending=[{file:entryPath,documentUrl}];
927
959
  const sharedFiles=new Set(records.filter(record=>record.appRelativePath===undefined).map(record=>record.destination));
960
+ const applicationFiles=new Set(records.filter(function applicationRecord(record){
961
+ return record.appRelativePath!==undefined;
962
+ }).map(function applicationDestination(record){return record.destination;}));
928
963
  for(const file of files){
929
964
  if((sharedFiles.has(file)||/^arcane\/(?:modules|entities|components|css|sdk|dependencies)\//u.test(file))
930
965
  &&/\.(?:m?js|html?|css)$/iu.test(file))pending.push({file,documentUrl});
@@ -985,7 +1020,9 @@ async function packageWithContext(context,options={}){
985
1020
  if(pwaEnabled&&offlineInventory.has(file)){
986
1021
  offlineReferences.add(versionAssetUrl(`.${target.pathname}${target.search}`,null));
987
1022
  }
988
- if(traversable)pending.push({
1023
+ const selectedDocument=kind!=='document'||!applicationFiles.has(file)||explicitDocumentPaths===null
1024
+ ||explicitDocumentPaths.has(file);
1025
+ if(traversable&&selectedDocument)pending.push({
989
1026
  file,
990
1027
  documentUrl:kind==='document'?target
991
1028
  :baseHref?new URL(baseHref,ownerUrl):current.documentUrl
@@ -195,10 +195,20 @@ npm run run
195
195
 
196
196
  The explicit \`import-map\` command refreshes
197
197
  \`${appPrefix}modules/arcane.importmap.json\` and the managed inline browser
198
- import map in every directly navigable descriptor-admitted \`.html\`/\`.htm\`
199
- document. HTML component fragments remain package files but do not receive a
200
- document-level base or managed import map.
201
- Development, package, and build refresh that shared inventory when the selected operation needs it.
198
+ import map in each selected application \`.html\`/\`.htm\` document.
199
+ Optional \`package.documents\` in \`arcane-app.json\` selects literal app-root-relative
200
+ HTML paths already included and not excluded, for example \`["index.html"]\`.
201
+ The entry is always selected first; \`[]\` selects only the entry. Omission
202
+ retains automatic discovery of the entry and included secondary pages with an
203
+ active base. The schema-1 package projection preserves this as \`documents\`,
204
+ including the distinction between omission and an empty list.
205
+ Selected pages retain their existing path-correct base requirement. With an
206
+ explicit list, other included HTML documents retain their exact content, even
207
+ when referenced by an iframe. Document selection leaves serving, packaging and
208
+ offline resource membership unchanged. HTML component processing and shared
209
+ runtime-resource traversal retain their existing behavior.
210
+ Development startup refreshes the selected managed maps; package and build
211
+ consume the saved source maps and the same document selection.
202
212
  Commit generated import maps and enabled offline app files. Hosting workflows consume those committed files rather than generating them.
203
213
  Named \`${directRuntime?'arcane-os/modules/* and arcane-os/entities/*':'arcane/*'}\` imports resolve through the managed map to the selected SDK files. Packaging copies the complete selected application, runtime, and specifier
204
214
  map to \`dist/${appId}\` without running application tests. Run \`verify\` only when
package/src/toolchain.mjs CHANGED
@@ -299,6 +299,7 @@ async function refreshPreparedImportMap(prepared,{signal,onEvent,workspaceOperat
299
299
  signal
300
300
  });
301
301
  if(inspected.entry!==manifest.entry
302
+ ||JSON.stringify(inspected.documents)!==JSON.stringify(manifest.documents)
302
303
  ||JSON.stringify(inspected.include)!==JSON.stringify(manifest.include)
303
304
  ||JSON.stringify(inspected.exclude)!==JSON.stringify(manifest.exclude)){
304
305
  throw new ArcaneError(