arcane-os 0.51.1 → 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/docs/roadmap.md CHANGED
@@ -1,22 +1,29 @@
1
1
  # Development roadmap
2
2
 
3
- The SDK contract keeps target names and operations stable while each current
4
- development provider gathers its remaining promotion evidence. Each item below
5
- is small enough to become a GitHub issue without combining every platform into
6
- one unbounded build.
3
+ The SDK already provides root application scaffolding, direct npm browser
4
+ routes, managed import maps, live source serving, browser/PWA packaging and
5
+ explicit native provider pairing. This roadmap distinguishes those implemented
6
+ contracts from remaining development work. Future items do not select an
7
+ implementation, dependency, publication or deployment.
7
8
 
8
- ## Publication prerequisites
9
+ ## Current delivery baseline
9
10
 
10
- - Mark the Arcane OS monorepo root package private or give it an internal-only
11
- package name before the first `arcane-os` publication.
12
- - Decide and document the license that permits proprietary applications to
13
- bundle the synchronized Arcane runtime.
14
- - Publish one fresh, never-reused prerelease candidate under only the npm `dev`
15
- tag, verify the tarball and
16
- provenance, add a second appropriate npm owner, and configure the exact
17
- trusted-publisher workflow.
11
+ - SDK publication uses the existing selected-package workflow. `dev` and
12
+ `latest` are npm channels; `main` remains the canonical source branch. See
13
+ [publishing](publishing.md) for the selected-artifact procedure.
14
+ - License terms and commercial licensing information are already documented in
15
+ [LICENSE](../LICENSE), [COMMERCIAL-LICENSE.md](../COMMERCIAL-LICENSE.md) and
16
+ [NOTICE](../NOTICE).
17
+ - Standalone apps use their repository root and installed npm resources.
18
+ Explicit multi-app and integrated workspaces retain their selected layout.
19
+ - The browser path includes PWA installation, offline resource selection,
20
+ conditional refresh and the shared update prompt. Development and packaged
21
+ preview serving share the published `node-http-server` MIME map.
22
+ - The reusable application release workflow remains unavailable until its
23
+ checked-in implementation matches the documented package-and-upload contract.
24
+ This is separate from SDK npm publication.
18
25
 
19
- ## App admission and native extraction
26
+ ## Native provider continuity
20
27
 
21
28
  - Keep the implemented process-local `arcane-native-build-plan/1` and
22
29
  `arcane-native-builder/1` lifecycle as the single provider seam for the CLI,
@@ -31,16 +38,11 @@ one unbounded build.
31
38
  - Preserve the implemented portable, Windows x64, Linux x64, Linux ARM64, and
32
39
  Android ARM64 providers as one selected app plus its exact declared dependency
33
40
  closure. Add new providers without weakening that release-reader boundary.
34
- - Extend the implemented same-process retained receipt lifecycle through an
35
- authenticated Arcane host broker only when persistent cross-process reuse or
36
- restartable app-scoped Core sessions are required.
37
- - Preserve exact packaged-artifact authority across browser and native plan,
38
- verify, and run boundaries; never regress to treating a mutable path or
39
- manifest file as a reusable receipt.
40
- - Add a manifest-declared origin policy for native enforcement and an optional
41
- policy-faithful development mode. The current capability-gated loopback host
42
- deliberately uses a broad development-only policy so the shared runtime can
43
- exercise remote providers, media, WebSockets, and embeds.
41
+ - Keep native build and launch within the paired provider's process lifecycle.
42
+ Persistent sessions or restartable host integration require a separately
43
+ selected capability and its owning host implementation.
44
+ - Preserve complete selected app releases through planning, build and run.
45
+ Explicit verification remains separate from the ordinary build/run lifecycle.
44
46
 
45
47
  ## Platform adapters
46
48
 
@@ -62,6 +64,9 @@ one unbounded build.
62
64
  reproducible. The APK contains no native ABI and is architecture-neutral. Add
63
65
  AAB, release signing, store publishing, and update continuity only as explicit
64
66
  later promotion work.
67
+ - macOS: the current registry provides browser delivery and no native package
68
+ adapter. A native adapter remains additional platform work; shared portable
69
+ SDK behavior must keep its declared macOS support.
65
70
 
66
71
  ## Developer experience
67
72
 
@@ -70,8 +75,9 @@ one unbounded build.
70
75
  development check, never app discovery or an arbitrary package-script loop.
71
76
  - Add the Arcane Developer control panel as a client of the same operation API;
72
77
  it stores local repository paths per user and never becomes a second builder.
73
- - Add authenticated GitHub artifact admission so Arcane OS installs verified
74
- releases without cloning proprietary source.
78
+ - Add an Arcane-owned source-development native wrapper around the same live
79
+ browser surface when that capability is selected. The current native `run`
80
+ command packages and builds its selected app; it is a different lifecycle.
75
81
  - Preserve the implemented integrated app-scoped native path: `--workspace` and
76
82
  `--arcane-root` identify the same checkout, one app and one target are
77
83
  selected, and `--output-root` remains outside the checkout. Future GUI clients
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.51.1",
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",
@@ -4448,7 +4448,8 @@ class AI {
4448
4448
  signal,
4449
4449
  payload.reasoningEffort,
4450
4450
  onRetry,
4451
- payload.model
4451
+ payload.model,
4452
+ payload.temperature
4452
4453
  );
4453
4454
  }
4454
4455
 
@@ -4480,7 +4481,8 @@ class AI {
4480
4481
  payload.reasoningEffort,
4481
4482
  observeToolText,
4482
4483
  onRetry,
4483
- payload.model
4484
+ payload.model,
4485
+ payload.temperature
4484
4486
  );
4485
4487
  }
4486
4488
 
@@ -4810,7 +4812,8 @@ class AI {
4810
4812
  normalizedReasoningEffort,
4811
4813
  observeToolText,
4812
4814
  onRetry,
4813
- model
4815
+ model,
4816
+ temperature
4814
4817
  );
4815
4818
  const structuralToolCalls=normalizeAICompletionToolCalls(
4816
4819
  completion
@@ -4920,7 +4923,8 @@ class AI {
4920
4923
  reasoningEffort,
4921
4924
  observeToolText=null,
4922
4925
  onRetry = null,
4923
- model=this.model
4926
+ model=this.model,
4927
+ temperature
4924
4928
  ){
4925
4929
  let speechTurnCompleted=false;
4926
4930
 
@@ -4949,6 +4953,10 @@ class AI {
4949
4953
  stream:true
4950
4954
  }
4951
4955
 
4956
+ if(this.llmService==='TWIN' && temperature!==undefined){
4957
+ request.temperature=temperature;
4958
+ }
4959
+
4952
4960
  if(structuredOutputFormat){
4953
4961
  request.response_format=openAIResponseFormat(
4954
4962
  structuredOutputFormat
@@ -5740,7 +5748,8 @@ class AI {
5740
5748
  signal,
5741
5749
  normalizedReasoningEffort,
5742
5750
  onRetry,
5743
- model
5751
+ model,
5752
+ temperature
5744
5753
  );
5745
5754
  }
5746
5755
 
@@ -5795,7 +5804,8 @@ class AI {
5795
5804
  signal=null,
5796
5805
  reasoningEffort,
5797
5806
  onRetry = null,
5798
- model=this.model
5807
+ model=this.model,
5808
+ temperature
5799
5809
  ){
5800
5810
  validateAIStructuralRequest(messages,tools,parallel_tool_calls);
5801
5811
  this.#assertServiceConfigured(this.llmService);
@@ -5819,6 +5829,10 @@ class AI {
5819
5829
  stream:false
5820
5830
  }
5821
5831
 
5832
+ if(this.llmService==='TWIN' && temperature!==undefined){
5833
+ request.temperature=temperature;
5834
+ }
5835
+
5822
5836
  if(structuredOutputFormat){
5823
5837
  request.response_format=openAIResponseFormat(
5824
5838
  structuredOutputFormat
@@ -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