arcane-os 0.1.0-dev.5
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 +154 -0
- package/COMMERCIAL-LICENSE.md +13 -0
- package/LICENSE +661 -0
- package/NOTICE +70 -0
- package/README.md +448 -0
- package/bin/arcane-test.mjs +741 -0
- package/bin/arcane.mjs +5 -0
- package/docs/architecture.md +234 -0
- package/docs/compatibility.md +36 -0
- package/docs/event-manager.md +166 -0
- package/docs/platform-targets.md +108 -0
- package/docs/publishing.md +203 -0
- package/docs/reference/README.md +117 -0
- package/docs/reference/arcane-ollama.md +288 -0
- package/docs/reference/availability-and-normalization.md +123 -0
- package/docs/reference/behavioral-testing.md +86 -0
- package/docs/reference/cli.md +569 -0
- package/docs/reference/core/README.md +62 -0
- package/docs/reference/core/arcane-ai-contracts.md +873 -0
- package/docs/reference/core/arcane-api.md +601 -0
- package/docs/reference/core/arcane-entities.md +65 -0
- package/docs/reference/core/arcane-events.md +134 -0
- package/docs/reference/core/ollama-module.md +181 -0
- package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +1909 -0
- package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +1057 -0
- package/docs/reference/core/reference/arcane-api/core-and-events.md +320 -0
- package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +610 -0
- package/docs/reference/core/reference/arcane-api/namespaces.md +1157 -0
- package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +1423 -0
- package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +315 -0
- package/docs/reference/event-manager.md +957 -0
- package/docs/reference/inventory/package-api.json +2632 -0
- package/docs/reference/inventory/runtime-components.json +934 -0
- package/docs/reference/inventory/runtime-entities.json +26 -0
- package/docs/reference/inventory/runtime-modules.json +1249 -0
- package/docs/reference/protocols.md +242 -0
- package/docs/reference/runtime-components.md +1098 -0
- package/docs/reference/runtime-entities.md +303 -0
- package/docs/reference/runtime-modules.md +2010 -0
- package/docs/reference/sdk-api.md +4901 -0
- package/docs/roadmap.md +79 -0
- package/docs/work-amplification.md +124 -0
- package/node_modules/event-pubsub/CHANGELOG.md +55 -0
- package/node_modules/event-pubsub/MIGRATION.md +70 -0
- package/node_modules/event-pubsub/README.md +363 -0
- package/node_modules/event-pubsub/SECURITY.md +37 -0
- package/node_modules/event-pubsub/index.js +141 -0
- package/node_modules/event-pubsub/licence +21 -0
- package/node_modules/event-pubsub/package.json +59 -0
- package/node_modules/strong-type/README.md +408 -0
- package/node_modules/strong-type/assets/strong-type-header.png +0 -0
- package/node_modules/strong-type/index.js +1151 -0
- package/node_modules/strong-type/licence +21 -0
- package/node_modules/strong-type/node.js +125 -0
- package/node_modules/strong-type/package.json +61 -0
- package/package.json +95 -0
- package/runtime/ARCANE_RUNTIME_RELEASE.json +791 -0
- package/runtime/arcane/components/app-bar.html +468 -0
- package/runtime/arcane/components/assistant-panel.html +715 -0
- package/runtime/arcane/components/calculator.html +8 -0
- package/runtime/arcane/components/chart.html +655 -0
- package/runtime/arcane/components/chat.html +1225 -0
- package/runtime/arcane/components/conversation-view.html +13 -0
- package/runtime/arcane/components/dashboard-config.html +341 -0
- package/runtime/arcane/components/data-maintenance.html +112 -0
- package/runtime/arcane/components/data-view.html +92 -0
- package/runtime/arcane/components/directory-picker.html +197 -0
- package/runtime/arcane/components/document-inspector.html +252 -0
- package/runtime/arcane/components/file-drop.html +264 -0
- package/runtime/arcane/components/file-inspector.html +293 -0
- package/runtime/arcane/components/file-manager.html +1715 -0
- package/runtime/arcane/components/header.html +142 -0
- package/runtime/arcane/components/integration-settings.html +14 -0
- package/runtime/arcane/components/local-ai-status.html +360 -0
- package/runtime/arcane/components/markdown-document.html +1048 -0
- package/runtime/arcane/components/markdown-editor.html +360 -0
- package/runtime/arcane/components/media-embed.html +8 -0
- package/runtime/arcane/components/modal.html +402 -0
- package/runtime/arcane/components/output-panel.html +259 -0
- package/runtime/arcane/components/preferences-form.html +135 -0
- package/runtime/arcane/components/record-timeline.html +105 -0
- package/runtime/arcane/components/relationship-board.html +116 -0
- package/runtime/arcane/components/screen-capture.html +8 -0
- package/runtime/arcane/components/source-code-viewer.html +441 -0
- package/runtime/arcane/components/source-explanation.html +124 -0
- package/runtime/arcane/components/speech.html +365 -0
- package/runtime/arcane/components/summary-strip.html +177 -0
- package/runtime/arcane/components/table.html +77 -0
- package/runtime/arcane/components/task-progress.html +282 -0
- package/runtime/arcane/components/terminal-workspace.html +65 -0
- package/runtime/arcane/components/theme-editor.html +41 -0
- package/runtime/arcane/components/theme-switcher.html +46 -0
- package/runtime/arcane/components/unified-inbox.html +20 -0
- package/runtime/arcane/components/voice-transcription.html +476 -0
- package/runtime/arcane/components/weather-widget.html +8 -0
- package/runtime/arcane/components/web-navigator.html +239 -0
- package/runtime/arcane/css/communications.css +1 -0
- package/runtime/arcane/css/dashboard-config.css +45 -0
- package/runtime/arcane/css/document-site.css +981 -0
- package/runtime/arcane/css/layout.css +438 -0
- package/runtime/arcane/css/primitives.css +321 -0
- package/runtime/arcane/css/theme.css +112 -0
- package/runtime/arcane/css/utility-workspace.css +1 -0
- package/runtime/arcane/entities/ApiModelRecord.js +20 -0
- package/runtime/arcane/entities/Calculation.js +13 -0
- package/runtime/arcane/entities/Chat.js +581 -0
- package/runtime/arcane/entities/CommunicationMessage.js +29 -0
- package/runtime/arcane/entities/CommunicationThread.js +21 -0
- package/runtime/arcane/entities/Document.js +10 -0
- package/runtime/arcane/entities/File.js +143 -0
- package/runtime/arcane/entities/Image.js +135 -0
- package/runtime/arcane/entities/IntentEnvelope.js +834 -0
- package/runtime/arcane/entities/Preference.js +83 -0
- package/runtime/arcane/entities/TWiNPolicyDecision.js +1092 -0
- package/runtime/arcane/entities/TerminalSession.js +46 -0
- package/runtime/arcane/entities/Theme.js +107 -0
- package/runtime/arcane/entities/User.js +1046 -0
- package/runtime/arcane/entities/Weather.js +23 -0
- package/runtime/arcane/img/arcane-os-everywhere.png +0 -0
- package/runtime/arcane/img/arrow-left.png +0 -0
- package/runtime/arcane/img/arrow-right.png +0 -0
- package/runtime/arcane/img/doc.svg +5 -0
- package/runtime/arcane/img/folder.svg +4 -0
- package/runtime/arcane/img/image.svg +5 -0
- package/runtime/arcane/img/refresh.png +0 -0
- package/runtime/arcane/img/send.svg +9 -0
- package/runtime/arcane/img/trash.svg +5 -0
- package/runtime/arcane/img/upload.svg +5 -0
- package/runtime/arcane/modules/AI.js +2048 -0
- package/runtime/arcane/modules/AIPreferenceRuntime.js +32 -0
- package/runtime/arcane/modules/AIPreferenceTuple.js +92 -0
- package/runtime/arcane/modules/AIResponseLength.js +42 -0
- package/runtime/arcane/modules/AIResponseURLPolicy.js +626 -0
- package/runtime/arcane/modules/AnsiText.js +53 -0
- package/runtime/arcane/modules/ApiModelDatabase.js +25 -0
- package/runtime/arcane/modules/AppDataScope.js +245 -0
- package/runtime/arcane/modules/AppearancePreferences.js +28 -0
- package/runtime/arcane/modules/ArcaneCommunicationBridge.js +22 -0
- package/runtime/arcane/modules/ArcaneNavigationPolicy.js +135 -0
- package/runtime/arcane/modules/ArcaneNetworkPolicy.js +255 -0
- package/runtime/arcane/modules/AsyncBoundary.js +161 -0
- package/runtime/arcane/modules/BrowserTestSuite.js +326 -0
- package/runtime/arcane/modules/CalculatorEngine.js +20 -0
- package/runtime/arcane/modules/CaseEvidenceIndexer.js +134 -0
- package/runtime/arcane/modules/ChartLibrary.js +35 -0
- package/runtime/arcane/modules/ChatRecords.js +13 -0
- package/runtime/arcane/modules/CommunicationAppController.js +43 -0
- package/runtime/arcane/modules/CommunicationHub.js +16 -0
- package/runtime/arcane/modules/CommunicationPreferences.js +13 -0
- package/runtime/arcane/modules/CommunicationProviderRegistry.js +16 -0
- package/runtime/arcane/modules/ComponentContracts.js +586 -0
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +288 -0
- package/runtime/arcane/modules/ConversationActionItems.js +488 -0
- package/runtime/arcane/modules/ConversationClosingReport.js +274 -0
- package/runtime/arcane/modules/ConversationTimebox.js +527 -0
- package/runtime/arcane/modules/CoreLocalModelCatalog.js +255 -0
- package/runtime/arcane/modules/DBLS.js +171 -0
- package/runtime/arcane/modules/DBOPFS.js +1154 -0
- package/runtime/arcane/modules/DBOPFSWorker.js +116 -0
- package/runtime/arcane/modules/DataMaintenance.js +91 -0
- package/runtime/arcane/modules/DevelopmentWorkspace.js +74 -0
- package/runtime/arcane/modules/DirectoryPicker.js +109 -0
- package/runtime/arcane/modules/DocumentNavigation.js +223 -0
- package/runtime/arcane/modules/Errors.js +1025 -0
- package/runtime/arcane/modules/GifEncoder.js +29 -0
- package/runtime/arcane/modules/HTMLImport.js +114 -0
- package/runtime/arcane/modules/InMemoryCommunicationProvider.js +14 -0
- package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +275 -0
- package/runtime/arcane/modules/LocalAIReadiness.js +870 -0
- package/runtime/arcane/modules/LocalAIReadinessController.js +156 -0
- package/runtime/arcane/modules/MD.js +111 -0
- package/runtime/arcane/modules/Mail.js +352 -0
- package/runtime/arcane/modules/MailTransport.mjs +180 -0
- package/runtime/arcane/modules/Marked.min.js +71 -0
- package/runtime/arcane/modules/MemoryRecords.js +44 -0
- package/runtime/arcane/modules/MessageAdvisory.js +38 -0
- package/runtime/arcane/modules/ModelDefinition.js +189 -0
- package/runtime/arcane/modules/Ollama.js +74 -0
- package/runtime/arcane/modules/OllamaModelIdentifier.js +21 -0
- package/runtime/arcane/modules/OllamaSettings.js +24 -0
- package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +14 -0
- package/runtime/arcane/modules/PreferenceStore.js +109 -0
- package/runtime/arcane/modules/QRCode.min.js +1 -0
- package/runtime/arcane/modules/Questionnaire.js +61 -0
- package/runtime/arcane/modules/RecordLinkIndex.js +24 -0
- package/runtime/arcane/modules/RecordPassageIndex.js +222 -0
- package/runtime/arcane/modules/RecordReviewStore.js +94 -0
- package/runtime/arcane/modules/RevocableProjectionLedger.js +1623 -0
- package/runtime/arcane/modules/RiskSignalAnalyzer.js +37 -0
- package/runtime/arcane/modules/ScamRiskPolicy.js +62 -0
- package/runtime/arcane/modules/ScopedOPFSCache.js +183 -0
- package/runtime/arcane/modules/ScreenCapture.js +20 -0
- package/runtime/arcane/modules/SpeechPlayback.js +581 -0
- package/runtime/arcane/modules/StaticDocumentCatalog.js +1248 -0
- package/runtime/arcane/modules/SystemAppearance.js +20 -0
- package/runtime/arcane/modules/SystemPlatformPresentation.js +51 -0
- package/runtime/arcane/modules/SystemToolRegistry.js +28 -0
- package/runtime/arcane/modules/TerminalClient.js +52 -0
- package/runtime/arcane/modules/TerminalCommandRegistry.js +50 -0
- package/runtime/arcane/modules/ThemeBootstrap.js +26 -0
- package/runtime/arcane/modules/ThemeManager.js +131 -0
- package/runtime/arcane/modules/TimeGuard.js +149 -0
- package/runtime/arcane/modules/ToolCallRouter.js +83 -0
- package/runtime/arcane/modules/WaitForComponent.js +102 -0
- package/runtime/arcane/modules/YouTubeMedia.js +16 -0
- package/runtime/arcane/modules/uPlot.LICENSE.txt +21 -0
- package/runtime/arcane/modules/uPlot.iife.min.js +2 -0
- package/runtime/arcane/modules/uPlot.min.css +1 -0
- package/runtime/arcane/security/arcane-network-policy.json +6 -0
- package/runtime/strong-type/index.js +352 -0
- package/runtime/strong-type/licence +21 -0
- package/runtime/strong-type/package.json +45 -0
- package/schemas/arcane-app-bundle.schema.json +125 -0
- package/schemas/arcane-app.schema.json +329 -0
- package/schemas/arcane-lock.schema.json +86 -0
- package/schemas/arcane-package.schema.json +224 -0
- package/schemas/cli-event.schema.json +122 -0
- package/schemas/event-stack.schema.json +152 -0
- package/schemas/native-build-plan.schema.json +176 -0
- package/schemas/target-adapter.schema.json +117 -0
- package/src/app-descriptor.mjs +500 -0
- package/src/cli/main.mjs +561 -0
- package/src/constants.mjs +31 -0
- package/src/dev-server.mjs +718 -0
- package/src/doctor.mjs +315 -0
- package/src/dom-event-instrumentation.mjs +594 -0
- package/src/errors.mjs +75 -0
- package/src/event-manager.mjs +1342 -0
- package/src/event-queue.mjs +138 -0
- package/src/events.mjs +219 -0
- package/src/index.mjs +177 -0
- package/src/integrated-provider-loader.mjs +432 -0
- package/src/native-plan.mjs +698 -0
- package/src/native-provider-loader.mjs +1126 -0
- package/src/packager/core.mjs +2691 -0
- package/src/process.mjs +353 -0
- package/src/release-bundle.mjs +2523 -0
- package/src/repository.mjs +90 -0
- package/src/runtime.mjs +452 -0
- package/src/scaffold.mjs +380 -0
- package/src/targets/index.mjs +436 -0
- package/src/templates/assets/app-icon.png +0 -0
- package/src/templates/workspace-template.mjs +388 -0
- package/src/testing-loader.mjs +9 -0
- package/src/testing.mjs +427 -0
- package/src/toolchain.mjs +1335 -0
- package/src/update-check.mjs +307 -0
- package/src/workspace.mjs +449 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
const DEFAULT_MAX_TESTS=64;
|
|
2
|
+
const HARD_MAX_TESTS=256;
|
|
3
|
+
const DEFAULT_TIMEOUT_MS=5000;
|
|
4
|
+
const HARD_TIMEOUT_MS=60000;
|
|
5
|
+
const MAX_MESSAGE_CHARACTERS=1000;
|
|
6
|
+
const CONTROL_CHARACTERS=/[\u0000-\u001f\u007f]/;
|
|
7
|
+
const TEST_ID_PATTERN=/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
8
|
+
const RESULT_STATUSES=new Set(['fail','pass','skip']);
|
|
9
|
+
|
|
10
|
+
function isPlainRecord(value){
|
|
11
|
+
return Boolean(value)
|
|
12
|
+
&&typeof value==='object'
|
|
13
|
+
&&!Array.isArray(value)
|
|
14
|
+
&&Object.getPrototypeOf(value)===Object.prototype;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function coded(error,code){
|
|
18
|
+
if(!error.code) error.code=code;
|
|
19
|
+
return error;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function fail(message,code,ErrorType=TypeError){
|
|
23
|
+
throw coded(new ErrorType(message),code);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function knownKeys(value,allowed,label,code='BROWSER_TEST_INVALID_OPTIONS'){
|
|
27
|
+
const unknown=Object.keys(value).find(key=>!allowed.has(key));
|
|
28
|
+
if(unknown) fail(`${label} contains an unsupported field: ${unknown}.`,code);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function boundedInteger(value,label,{minimum,maximum}){
|
|
32
|
+
if(!Number.isSafeInteger(value)||value<minimum||value>maximum){
|
|
33
|
+
fail(`${label} must be an integer from ${minimum} through ${maximum}.`,'BROWSER_TEST_INVALID_LIMIT',RangeError);
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function descriptorText(value,label,maximum){
|
|
39
|
+
if(typeof value!=='string') fail(`${label} must be a string.`,'BROWSER_TEST_INVALID_DESCRIPTOR');
|
|
40
|
+
const normalized=value.trim();
|
|
41
|
+
if(!normalized) fail(`${label} cannot be empty.`,'BROWSER_TEST_INVALID_DESCRIPTOR');
|
|
42
|
+
if(normalized.length>maximum) fail(`${label} exceeds ${maximum} characters.`,'BROWSER_TEST_INVALID_DESCRIPTOR',RangeError);
|
|
43
|
+
if(CONTROL_CHARACTERS.test(normalized)) fail(`${label} cannot contain control characters.`,'BROWSER_TEST_INVALID_DESCRIPTOR');
|
|
44
|
+
if(normalized!==normalized.normalize('NFC')) fail(`${label} must use Unicode NFC normalization.`,'BROWSER_TEST_INVALID_DESCRIPTOR');
|
|
45
|
+
return normalized;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resultMessage(value,fallback){
|
|
49
|
+
if(value===undefined||value===null||value==='') return fallback;
|
|
50
|
+
const message=String(value)
|
|
51
|
+
.replace(/[\u0000-\u001f\u007f]+/g,' ')
|
|
52
|
+
.trim();
|
|
53
|
+
return (message||fallback).slice(0,MAX_MESSAGE_CHARACTERS);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizeTests(value,{maxTests,timeoutMs}){
|
|
57
|
+
if(!Array.isArray(value)) fail('tests must be an array.','BROWSER_TEST_INVALID_OPTIONS');
|
|
58
|
+
if(value.length>maxTests) fail(`Test suite exceeds the ${maxTests}-test limit.`,'BROWSER_TEST_LIMIT',RangeError);
|
|
59
|
+
const seen=new Set();
|
|
60
|
+
return Object.freeze(value.map((item,index)=>{
|
|
61
|
+
if(!isPlainRecord(item)) fail(`Test descriptor ${index+1} must be a plain object.`,'BROWSER_TEST_INVALID_DESCRIPTOR');
|
|
62
|
+
knownKeys(item,new Set(['id','name','run','timeoutMs']),`Test descriptor ${index+1}`,'BROWSER_TEST_INVALID_DESCRIPTOR');
|
|
63
|
+
const id=descriptorText(item.id,`Test descriptor ${index+1} id`,128);
|
|
64
|
+
if(!TEST_ID_PATTERN.test(id)) fail(`Test descriptor ${index+1} has an invalid id.`,'BROWSER_TEST_INVALID_DESCRIPTOR');
|
|
65
|
+
const key=id.toLowerCase();
|
|
66
|
+
if(seen.has(key)) fail(`Test descriptors contain a case-colliding id: ${id}.`,'BROWSER_TEST_CASE_COLLISION');
|
|
67
|
+
seen.add(key);
|
|
68
|
+
if(typeof item.run!=='function') fail(`Test descriptor ${index+1} run must be a function.`,'BROWSER_TEST_INVALID_DESCRIPTOR');
|
|
69
|
+
return Object.freeze({
|
|
70
|
+
id,
|
|
71
|
+
name:descriptorText(item.name,`Test descriptor ${index+1} name`,256),
|
|
72
|
+
run:item.run,
|
|
73
|
+
timeoutMs:boundedInteger(item.timeoutMs??timeoutMs,`Test descriptor ${index+1} timeoutMs`,{minimum:10,maximum:timeoutMs}),
|
|
74
|
+
});
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function defaultNow(){
|
|
79
|
+
return globalThis.performance?.now?.()??Date.now();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeOptions(input){
|
|
83
|
+
if(!isPlainRecord(input)) fail('Browser test suite options must be a plain object.','BROWSER_TEST_INVALID_OPTIONS');
|
|
84
|
+
knownKeys(input,new Set(['maxTests','now','tests','timeoutMs']),'Browser test suite options');
|
|
85
|
+
const maxTests=boundedInteger(input.maxTests??DEFAULT_MAX_TESTS,'maxTests',{minimum:1,maximum:HARD_MAX_TESTS});
|
|
86
|
+
const timeoutMs=boundedInteger(input.timeoutMs??DEFAULT_TIMEOUT_MS,'timeoutMs',{minimum:10,maximum:HARD_TIMEOUT_MS});
|
|
87
|
+
const now=input.now??defaultNow;
|
|
88
|
+
if(typeof now!=='function') fail('now must be a function.','BROWSER_TEST_INVALID_OPTIONS');
|
|
89
|
+
return Object.freeze({
|
|
90
|
+
maxTests,
|
|
91
|
+
now,
|
|
92
|
+
tests:normalizeTests(input.tests??[],{maxTests,timeoutMs}),
|
|
93
|
+
timeoutMs,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalizeRunOptions(input){
|
|
98
|
+
if(!isPlainRecord(input)) fail('Test run options must be a plain object.','BROWSER_TEST_INVALID_OPTIONS');
|
|
99
|
+
knownKeys(input,new Set(['context','signal']),'Test run options');
|
|
100
|
+
const signal=input.signal??null;
|
|
101
|
+
if(signal!==null&&(
|
|
102
|
+
typeof signal!=='object'
|
|
103
|
+
||typeof signal.aborted!=='boolean'
|
|
104
|
+
||typeof signal.addEventListener!=='function'
|
|
105
|
+
||typeof signal.removeEventListener!=='function'
|
|
106
|
+
)) fail('signal must be an AbortSignal.','BROWSER_TEST_INVALID_OPTIONS');
|
|
107
|
+
return Object.freeze({context:input.context,signal});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function detailEvent(type,detail){
|
|
111
|
+
if(typeof globalThis.CustomEvent==='function') return new CustomEvent(type,{detail});
|
|
112
|
+
const event=new Event(type);
|
|
113
|
+
Object.defineProperty(event,'detail',{enumerable:true,value:detail});
|
|
114
|
+
return event;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function elapsed(now,start){
|
|
118
|
+
const end=Number(now());
|
|
119
|
+
if(!Number.isFinite(end)) fail('now() must return a finite number.','BROWSER_TEST_INVALID_CLOCK');
|
|
120
|
+
return Math.round(Math.max(0,end-start)*1000)/1000;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function startTime(now){
|
|
124
|
+
const value=Number(now());
|
|
125
|
+
if(!Number.isFinite(value)) fail('now() must return a finite number.','BROWSER_TEST_INVALID_CLOCK');
|
|
126
|
+
return value;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function skipError(message){
|
|
130
|
+
const error=coded(new Error(resultMessage(message,'Skipped by the check.')),'BROWSER_TEST_SKIP');
|
|
131
|
+
error.name='BrowserTestSkip';
|
|
132
|
+
return error;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function assertionError(message){
|
|
136
|
+
const error=coded(new Error(resultMessage(message,'The browser check failed.')),'BROWSER_TEST_ASSERTION');
|
|
137
|
+
error.name='BrowserTestAssertionError';
|
|
138
|
+
return error;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function abortError(){
|
|
142
|
+
const error=coded(new Error('The browser test run was aborted.'),'BROWSER_TEST_ABORTED');
|
|
143
|
+
error.name='AbortError';
|
|
144
|
+
return error;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function timeoutError(milliseconds){
|
|
148
|
+
const error=coded(new Error(`The check exceeded ${milliseconds} milliseconds.`),'BROWSER_TEST_TIMEOUT');
|
|
149
|
+
error.name='BrowserTestTimeoutError';
|
|
150
|
+
return error;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function runWithTimeout(callback,{signal,timeoutMs}){
|
|
154
|
+
if(signal?.aborted) return Promise.reject(abortError());
|
|
155
|
+
const controller=new AbortController();
|
|
156
|
+
return new Promise((resolve,reject)=>{
|
|
157
|
+
let settled=false;
|
|
158
|
+
const finish=(handler,value)=>{
|
|
159
|
+
if(settled) return;
|
|
160
|
+
settled=true;
|
|
161
|
+
clearTimeout(timer);
|
|
162
|
+
signal?.removeEventListener('abort',onAbort);
|
|
163
|
+
handler(value);
|
|
164
|
+
};
|
|
165
|
+
const onAbort=()=>{
|
|
166
|
+
controller.abort();
|
|
167
|
+
finish(reject,abortError());
|
|
168
|
+
};
|
|
169
|
+
const timer=setTimeout(()=>{
|
|
170
|
+
controller.abort();
|
|
171
|
+
finish(reject,timeoutError(timeoutMs));
|
|
172
|
+
},timeoutMs);
|
|
173
|
+
signal?.addEventListener('abort',onAbort,{once:true});
|
|
174
|
+
Promise.resolve()
|
|
175
|
+
.then(()=>callback(controller.signal))
|
|
176
|
+
.then(value=>finish(resolve,value),error=>finish(reject,error));
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function normalizedOutcome(value){
|
|
181
|
+
if(value===undefined||value===true){
|
|
182
|
+
return Object.freeze({status:'pass',message:'Passed.'});
|
|
183
|
+
}
|
|
184
|
+
if(value===false){
|
|
185
|
+
return Object.freeze({status:'fail',message:'The check returned false.',code:'BROWSER_TEST_ASSERTION'});
|
|
186
|
+
}
|
|
187
|
+
if(!isPlainRecord(value)){
|
|
188
|
+
return Object.freeze({status:'fail',message:'The check returned an invalid result.',code:'BROWSER_TEST_INVALID_RESULT'});
|
|
189
|
+
}
|
|
190
|
+
const unknown=Object.keys(value).find(key=>!new Set(['message','status']).has(key));
|
|
191
|
+
if(unknown||!RESULT_STATUSES.has(value.status)){
|
|
192
|
+
return Object.freeze({status:'fail',message:'The check returned an invalid result.',code:'BROWSER_TEST_INVALID_RESULT'});
|
|
193
|
+
}
|
|
194
|
+
const fallback=value.status==='pass'?'Passed.':value.status==='skip'?'Skipped.':'Failed.';
|
|
195
|
+
return Object.freeze({status:value.status,message:resultMessage(value.message,fallback)});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function outcomeFromError(error){
|
|
199
|
+
if(error?.code==='BROWSER_TEST_SKIP'){
|
|
200
|
+
return Object.freeze({status:'skip',message:resultMessage(error.message,'Skipped.'),code:error.code});
|
|
201
|
+
}
|
|
202
|
+
if(error?.code==='BROWSER_TEST_ABORTED'){
|
|
203
|
+
return Object.freeze({status:'skip',message:resultMessage(error.message,'The run was aborted.'),code:error.code});
|
|
204
|
+
}
|
|
205
|
+
return Object.freeze({
|
|
206
|
+
status:'fail',
|
|
207
|
+
message:resultMessage(error?.message,'The check failed.'),
|
|
208
|
+
code:typeof error?.code==='string'?error.code.slice(0,64):'BROWSER_TEST_ERROR',
|
|
209
|
+
errorName:resultMessage(error?.name,'Error').slice(0,128),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function resultRecord(test,outcome,durationMs){
|
|
214
|
+
return Object.freeze({
|
|
215
|
+
id:test.id,
|
|
216
|
+
name:test.name,
|
|
217
|
+
status:outcome.status,
|
|
218
|
+
message:outcome.message,
|
|
219
|
+
...(outcome.code?{code:outcome.code}:{}),
|
|
220
|
+
...(outcome.errorName?{errorName:outcome.errorName}:{}),
|
|
221
|
+
durationMs,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function skippedResult(test,message,code='BROWSER_TEST_ABORTED'){
|
|
226
|
+
return resultRecord(test,{status:'skip',message,code},0);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Runs a fixed inventory of parent-supplied browser checks sequentially.
|
|
231
|
+
*
|
|
232
|
+
* Test callbacks are trusted executable code supplied by the parent. This
|
|
233
|
+
* module never accepts source text, evaluates code, persists results, or
|
|
234
|
+
* selects application policy. Per-test abort signals and timeout races bound
|
|
235
|
+
* cooperative asynchronous orchestration; callbacks must still avoid blocking
|
|
236
|
+
* the page and stop work they started after their abort signal fires.
|
|
237
|
+
*/
|
|
238
|
+
export default class BrowserTestSuite extends EventTarget{
|
|
239
|
+
#now;
|
|
240
|
+
#running=false;
|
|
241
|
+
#tests;
|
|
242
|
+
|
|
243
|
+
constructor(options={}){
|
|
244
|
+
super();
|
|
245
|
+
const normalized=normalizeOptions(options);
|
|
246
|
+
this.#now=normalized.now;
|
|
247
|
+
this.#tests=normalized.tests;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
get running(){return this.#running;}
|
|
251
|
+
|
|
252
|
+
list(){
|
|
253
|
+
return Object.freeze(this.#tests.map(test=>Object.freeze({
|
|
254
|
+
id:test.id,
|
|
255
|
+
name:test.name,
|
|
256
|
+
timeoutMs:test.timeoutMs,
|
|
257
|
+
})));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
#emit(type,detail){
|
|
261
|
+
this.dispatchEvent(detailEvent(type,Object.freeze(detail)));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async run(options={}){
|
|
265
|
+
if(this.#running) fail('A browser test run is already active.','BROWSER_TEST_BUSY',Error);
|
|
266
|
+
const settings=normalizeRunOptions(options);
|
|
267
|
+
const suiteStart=startTime(this.#now);
|
|
268
|
+
const results=[];
|
|
269
|
+
let aborted=Boolean(settings.signal?.aborted);
|
|
270
|
+
this.#running=true;
|
|
271
|
+
this.#emit('browser-test-suite-start',{tests:this.list(),total:this.#tests.length});
|
|
272
|
+
try{
|
|
273
|
+
for(let index=0;index<this.#tests.length;index++){
|
|
274
|
+
const descriptor=this.#tests[index];
|
|
275
|
+
if(aborted||settings.signal?.aborted){
|
|
276
|
+
aborted=true;
|
|
277
|
+
const result=skippedResult(descriptor,'Skipped because the browser test run was aborted.');
|
|
278
|
+
results.push(result);
|
|
279
|
+
this.#emit('browser-test-result',{index,result,total:this.#tests.length});
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
this.#emit('browser-test-start',{
|
|
283
|
+
index,
|
|
284
|
+
test:Object.freeze({id:descriptor.id,name:descriptor.name,timeoutMs:descriptor.timeoutMs}),
|
|
285
|
+
total:this.#tests.length,
|
|
286
|
+
});
|
|
287
|
+
const testStart=startTime(this.#now);
|
|
288
|
+
let outcome;
|
|
289
|
+
try{
|
|
290
|
+
const value=await runWithTimeout(signal=>descriptor.run(Object.freeze({
|
|
291
|
+
assert(condition,message){if(!condition) throw assertionError(message);},
|
|
292
|
+
context:settings.context,
|
|
293
|
+
signal,
|
|
294
|
+
skip(message){throw skipError(message);},
|
|
295
|
+
})),{signal:settings.signal,timeoutMs:descriptor.timeoutMs});
|
|
296
|
+
outcome=normalizedOutcome(value);
|
|
297
|
+
}catch(error){
|
|
298
|
+
outcome=outcomeFromError(error);
|
|
299
|
+
if(error?.code==='BROWSER_TEST_ABORTED') aborted=true;
|
|
300
|
+
}
|
|
301
|
+
const result=resultRecord(descriptor,outcome,elapsed(this.#now,testStart));
|
|
302
|
+
results.push(result);
|
|
303
|
+
this.#emit('browser-test-result',{index,result,total:this.#tests.length});
|
|
304
|
+
}
|
|
305
|
+
}finally{
|
|
306
|
+
this.#running=false;
|
|
307
|
+
}
|
|
308
|
+
const totals=Object.freeze({
|
|
309
|
+
total:results.length,
|
|
310
|
+
pass:results.filter(result=>result.status==='pass').length,
|
|
311
|
+
fail:results.filter(result=>result.status==='fail').length,
|
|
312
|
+
skip:results.filter(result=>result.status==='skip').length,
|
|
313
|
+
});
|
|
314
|
+
const status=aborted?'aborted':totals.fail?'fail':totals.pass?'pass':'skip';
|
|
315
|
+
const summary=Object.freeze({
|
|
316
|
+
status,
|
|
317
|
+
totals,
|
|
318
|
+
durationMs:elapsed(this.#now,suiteStart),
|
|
319
|
+
results:Object.freeze(results),
|
|
320
|
+
});
|
|
321
|
+
this.#emit('browser-test-suite-complete',summary);
|
|
322
|
+
return summary;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export {assertionError,skipError};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import Calculation from '../entities/Calculation.js';
|
|
2
|
+
|
|
3
|
+
const FUNCTIONS=Object.freeze({sqrt:Math.sqrt,abs:Math.abs,sin:Math.sin,cos:Math.cos,tan:Math.tan,log:Math.log10,ln:Math.log});
|
|
4
|
+
const CONSTANTS=Object.freeze({pi:Math.PI,e:Math.E});
|
|
5
|
+
|
|
6
|
+
function tokenize(input){const source=String(input??'').trim();if(!source||source.length>512)throw new TypeError('Expression must contain 1-512 characters.');const tokens=[];let index=0;while(index<source.length){const rest=source.slice(index);const whitespace=rest.match(/^\s+/);if(whitespace){index+=whitespace[0].length;continue}const numeric=rest.match(/^(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?/i);if(numeric){tokens.push({type:'number',value:Number(numeric[0])});index+=numeric[0].length;continue}const identifier=rest.match(/^[a-z]+/i);if(identifier){tokens.push({type:'name',value:identifier[0].toLowerCase()});index+=identifier[0].length;continue}const symbol=source[index];if('+-*/%^()'.includes(symbol)){tokens.push({type:symbol,value:symbol});index++;continue}throw new SyntaxError(`Unexpected character at position ${index+1}.`)}tokens.push({type:'end'});return tokens;}
|
|
7
|
+
|
|
8
|
+
export function evaluateExpression(input){const tokens=tokenize(input);let cursor=0;const peek=()=>tokens[cursor];const take=type=>{if(peek().type!==type)throw new SyntaxError(`Expected ${type}.`);return tokens[cursor++]};
|
|
9
|
+
function primary(){const token=peek();if(token.type==='number'){cursor++;return token.value}if(token.type==='name'){cursor++;if(Object.hasOwn(CONSTANTS,token.value))return CONSTANTS[token.value];const fn=FUNCTIONS[token.value];if(!fn)throw new SyntaxError(`Unknown function: ${token.value}.`);take('(');const value=expression();take(')');return fn(value)}if(token.type==='('){cursor++;const value=expression();take(')');return value}throw new SyntaxError('Expected a number, constant, function, or parenthesized expression.')}
|
|
10
|
+
function unary(){if(peek().type==='+'){cursor++;return unary()}if(peek().type==='-'){cursor++;return -unary()}return primary()}
|
|
11
|
+
function power(){let value=unary();if(peek().type==='^'){cursor++;value=Math.pow(value,power())}return value}
|
|
12
|
+
function product(){let value=power();while(['*','/','%'].includes(peek().type)){const operator=tokens[cursor++].type,right=power();if((operator==='/'||operator==='%')&&right===0)throw new RangeError('Division by zero is undefined.');value=operator==='*'?value*right:operator==='/'?value/right:value%right}return value}
|
|
13
|
+
function expression(){let value=product();while(['+','-'].includes(peek().type)){const operator=tokens[cursor++].type,right=product();value=operator==='+'?value+right:value-right}return value}
|
|
14
|
+
const result=expression();if(peek().type!=='end')throw new SyntaxError('Unexpected content after the expression.');if(!Number.isFinite(result))throw new RangeError('The calculation did not produce a finite result.');return result;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default class CalculatorEngine extends EventTarget{
|
|
18
|
+
calculate(expression){try{const calculation=new Calculation({expression,result:evaluateExpression(expression)});this.dispatchEvent(event('calculator-result',calculation));return calculation}catch(error){this.dispatchEvent(event('calculator-error',{expression:String(expression??''),error}));throw error}}
|
|
19
|
+
}
|
|
20
|
+
function event(type,detail){return new CustomEvent(type,{detail});}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import {createHash} from 'node:crypto';
|
|
2
|
+
import {readdir,readFile,writeFile,mkdir} from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const sha256=bytes=>createHash('sha256').update(bytes).digest('hex');
|
|
6
|
+
const natural=(a,b)=>a.localeCompare(b,undefined,{numeric:true,sensitivity:'base'});
|
|
7
|
+
const stem=value=>value.replace(/\.[^.]+$/,'');
|
|
8
|
+
const safeName=value=>value.replace(/[<>:"/\\|?*\x00-\x1f]/g,' ').replace(/\s+/g,' ').trim();
|
|
9
|
+
|
|
10
|
+
function parseStructuredRecordName(name,{
|
|
11
|
+
pattern=/^(?<date>\d{2}-\d{2}-\d{2})\s+\[(?<source>[^\]]+)\]\s+-\s+(?<title>.+?)(?<extension>\.[^.]+)$/
|
|
12
|
+
}={}){
|
|
13
|
+
const match=String(name||'').match(pattern);
|
|
14
|
+
if(!match?.groups) return null;
|
|
15
|
+
const [year,month,day]=match.groups.date.split('-').map(Number);
|
|
16
|
+
const isoDate=`20${String(year).padStart(2,'0')}-${String(month).padStart(2,'0')}-${String(day).padStart(2,'0')}`;
|
|
17
|
+
const parsedDate=new Date(`${isoDate}T00:00:00Z`);
|
|
18
|
+
const dateValid=parsedDate.getUTCFullYear()===2000+year&&parsedDate.getUTCMonth()+1===month&&parsedDate.getUTCDate()===day;
|
|
19
|
+
return {
|
|
20
|
+
dateToken:match.groups.date,
|
|
21
|
+
isoDate:dateValid?isoDate:null,
|
|
22
|
+
source:safeName(match.groups.source),
|
|
23
|
+
title:safeName(match.groups.title),
|
|
24
|
+
extension:match.groups.extension.toLowerCase()
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function nearestPageMarker(text,index,pattern=/^#{1,6}\s+(?:Page|PDF Page)\s+(\d+)\b/gim){
|
|
29
|
+
const matches=[...text.slice(0,index).matchAll(pattern)];
|
|
30
|
+
return matches.length?Number(matches.at(-1)[1]):null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeEvidenceLabel(value=''){
|
|
34
|
+
return String(value)
|
|
35
|
+
.normalize('NFKD')
|
|
36
|
+
.replace(/[\u0300-\u036f]/g,'')
|
|
37
|
+
.replace(/[\u2018\u2019']/g,'')
|
|
38
|
+
.replace(/[^a-z0-9.]+/gi,' ')
|
|
39
|
+
.trim()
|
|
40
|
+
.toLowerCase();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function evidenceLabelTargets(title=''){
|
|
44
|
+
const value=String(title||'').trim();
|
|
45
|
+
const targets=new Set([normalizeEvidenceLabel(value)].filter(Boolean));
|
|
46
|
+
const label=value.match(/\b(Exhibit|Attachment)\s+([A-Za-z0-9]+(?:\.[A-Za-z0-9]+)?)/i);
|
|
47
|
+
const qualified=/\b(?:Petitioner|Respondent)(?:['\u2019]s)?\s+(?:Exhibit|Attachment)\b/i.test(value);
|
|
48
|
+
if(label&&!qualified&&(/^[A-Za-z]{1,3}(?:\.\d+)?$/.test(label[2])||/^\d+$/.test(label[2]))){
|
|
49
|
+
targets.add(normalizeEvidenceLabel(`${label[1]} ${label[2]}`));
|
|
50
|
+
}
|
|
51
|
+
return [...targets];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function renderedPageBlocks(markdown=''){
|
|
55
|
+
const text=String(markdown||'');
|
|
56
|
+
const markerPattern=/_rendered_pages[\\/][^\r\n]*?[\\/]page-(\d+)\.png\b/gi;
|
|
57
|
+
const markers=[...text.matchAll(markerPattern)]
|
|
58
|
+
.map(match=>({page:Number(match[1]),marker:match[0],index:match.index}))
|
|
59
|
+
.filter(item=>Number.isSafeInteger(item.page)&&item.page>0);
|
|
60
|
+
return markers.map((item,index)=>({
|
|
61
|
+
...item,
|
|
62
|
+
end:markers[index+1]?.index??text.length,
|
|
63
|
+
text:text.slice(item.index,markers[index+1]?.index??text.length)
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function standaloneEvidenceLabel(line='',targets=[]){
|
|
68
|
+
let value=normalizeEvidenceLabel(line);
|
|
69
|
+
value=value.replace(/^\d{2}(?:fl|dv)\d{6}\s+/,'').replace(/^\d{1,3}\s+/,'');
|
|
70
|
+
return targets.some(target=>value===target||new RegExp(`^${target.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')}\\s+(?:page\\s+)?\\d{1,3}$`,'i').test(value));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function resolveEvidenceSourcePage(markdown='',boundaryIndex=0,title=''){
|
|
74
|
+
const pages=renderedPageBlocks(markdown);
|
|
75
|
+
const containing=pages.find(page=>boundaryIndex>=page.index&&boundaryIndex<page.end);
|
|
76
|
+
if(containing){
|
|
77
|
+
return {sourcePage:containing.page,sourcePageStatus:'resolved',sourcePageMethod:'containing-rendered-page',sourcePageMarker:containing.marker,sourcePageCandidates:[containing.page]};
|
|
78
|
+
}
|
|
79
|
+
const targets=evidenceLabelTargets(title);
|
|
80
|
+
const candidates=pages.filter(page=>page.text.split(/\r?\n/).some(line=>standaloneEvidenceLabel(line,targets)));
|
|
81
|
+
const candidatePages=[...new Set(candidates.map(item=>item.page))].sort((left,right)=>left-right);
|
|
82
|
+
if(candidatePages.length===1){
|
|
83
|
+
const page=pages.find(item=>item.page===candidatePages[0]);
|
|
84
|
+
return {sourcePage:page.page,sourcePageStatus:'resolved',sourcePageMethod:'unique-standalone-label',sourcePageMarker:page.marker,sourcePageCandidates:candidatePages};
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
sourcePage:null,
|
|
88
|
+
sourcePageStatus:candidatePages.length?'ambiguous':'unresolved',
|
|
89
|
+
sourcePageMethod:candidatePages.length?'multiple-standalone-labels':null,
|
|
90
|
+
sourcePageMarker:null,
|
|
91
|
+
sourcePageCandidates:candidatePages
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function indexPairedRecord({
|
|
96
|
+
rawRoot,markdownRoot,evidenceOutputRoot,rawExtension='.pdf',
|
|
97
|
+
evidenceBoundary=/^#{2,6}\s+((?:Exhibit|Attachment)\b[^\r\n]*)/gim,
|
|
98
|
+
signalRules=[],recordIdPrefix='F',evidenceIdPrefix='E',buildEvidenceMarkdown
|
|
99
|
+
}){
|
|
100
|
+
if(!rawRoot||!markdownRoot||!evidenceOutputRoot) throw new TypeError('Raw, Markdown, and evidence output roots are required.');
|
|
101
|
+
await mkdir(evidenceOutputRoot,{recursive:true});
|
|
102
|
+
const rawNames=(await readdir(rawRoot)).filter(name=>name.toLowerCase().endsWith(rawExtension.toLowerCase())).sort(natural);
|
|
103
|
+
const markdownNames=(await readdir(markdownRoot)).filter(name=>/\.md$/i.test(name)).sort(natural);
|
|
104
|
+
const markdownByStem=new Map(markdownNames.map(name=>[stem(name).toLowerCase(),name]));
|
|
105
|
+
const records=[]; const evidence=[];
|
|
106
|
+
for(let index=0;index<rawNames.length;index++){
|
|
107
|
+
const rawName=rawNames[index]; const rawBytes=await readFile(path.join(rawRoot,rawName));
|
|
108
|
+
const hash=sha256(rawBytes); const markdownName=markdownByStem.get(stem(rawName).toLowerCase())||null;
|
|
109
|
+
const recordEvidence=[]; let signals=[];
|
|
110
|
+
if(markdownName){
|
|
111
|
+
const markdown=await readFile(path.join(markdownRoot,markdownName),'utf8');
|
|
112
|
+
signals=signalRules.filter(rule=>rule.pattern.test(markdown)).map(rule=>rule.id);
|
|
113
|
+
evidenceBoundary.lastIndex=0;
|
|
114
|
+
const boundaries=[...markdown.matchAll(evidenceBoundary)];
|
|
115
|
+
for(let n=0;n<boundaries.length;n++){
|
|
116
|
+
const match=boundaries[n]; const body=markdown.slice(match.index,boundaries[n+1]?.index??markdown.length).trim();
|
|
117
|
+
if(body.length<20) continue;
|
|
118
|
+
const id=`${evidenceIdPrefix}${String(evidence.length+1).padStart(4,'0')}`;
|
|
119
|
+
const title=safeName(match[1]); const pageResolution=resolveEvidenceSourcePage(markdown,match.index,title);
|
|
120
|
+
// Keep generated evidence paths stable and short. Descriptive source and
|
|
121
|
+
// exhibit labels remain in the record and Markdown instead of the path.
|
|
122
|
+
const fileName=`${id}.md`;
|
|
123
|
+
const item={id,title,parentRaw:rawName,markdown:markdownName,...pageResolution,file:`Evidence/MD/${fileName}`,parentSha256:hash,body};
|
|
124
|
+
const output=buildEvidenceMarkdown?buildEvidenceMarkdown(item):`# ${title}\n\n- Evidence ID: ${id}\n- Parent source: ${rawName}\n- Related Markdown: ${markdownName}\n- Source page: ${item.sourcePage??'not resolved from Markdown'}\n- Source page status: ${item.sourcePageStatus}\n- Source page method: ${item.sourcePageMethod??'none'}\n- Source page marker: ${item.sourcePageMarker??'none'}\n- Source page candidates: ${item.sourcePageCandidates.join(', ')||'none'}\n- Parent SHA-256: ${hash}\n\n${body}\n`;
|
|
125
|
+
await writeFile(path.join(evidenceOutputRoot,fileName),output,'utf8'); evidence.push(item); recordEvidence.push(id);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
records.push({id:`${recordIdPrefix}${String(index+1).padStart(4,'0')}`,name:rawName,markdown:markdownName,status:markdownName?'paired':'missing-markdown',sha256:hash,size:rawBytes.length,signals,evidence:recordEvidence,reviewStatus:'not-reviewed'});
|
|
129
|
+
}
|
|
130
|
+
const rawStems=new Set(rawNames.map(name=>stem(name).toLowerCase()));
|
|
131
|
+
return {records,evidence,markdownNames,orphanMarkdown:markdownNames.filter(name=>!rawStems.has(stem(name).toLowerCase()))};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export {indexPairedRecord,nearestPageMarker,parseStructuredRecordName,renderedPageBlocks,resolveEvidenceSourcePage,safeName,sha256,stem};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
let libraryPromise=null;
|
|
2
|
+
|
|
3
|
+
function loadChartLibrary(){
|
|
4
|
+
if(window.uPlot){
|
|
5
|
+
return Promise.resolve(window.uPlot);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if(libraryPromise){
|
|
9
|
+
return libraryPromise;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
libraryPromise=new Promise(
|
|
13
|
+
function loadChartLibraryPromise(resolve,reject){
|
|
14
|
+
const script=document.createElement('script');
|
|
15
|
+
|
|
16
|
+
script.src=new URL('./uPlot.iife.min.js',import.meta.url).href;
|
|
17
|
+
script.addEventListener(
|
|
18
|
+
'load',
|
|
19
|
+
()=>window.uPlot
|
|
20
|
+
?resolve(window.uPlot)
|
|
21
|
+
:reject(new Error('uPlot did not initialize'))
|
|
22
|
+
);
|
|
23
|
+
script.addEventListener(
|
|
24
|
+
'error',
|
|
25
|
+
()=>reject(new Error('Unable to load the chart library'))
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
document.head.append(script);
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
return libraryPromise;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default loadChartLibrary;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import ArcaneCommunicationBridge from './ArcaneCommunicationBridge.js';
|
|
2
|
+
import CommunicationHub from './CommunicationHub.js?v=2';
|
|
3
|
+
import CommunicationPreferences from './CommunicationPreferences.js';
|
|
4
|
+
import {loadAndApplyTheme} from './ThemeManager.js';
|
|
5
|
+
import {inspectMessageRecords,unavailableMessageInspection} from './MessageAdvisory.js?v=3';
|
|
6
|
+
|
|
7
|
+
function defaults(services){return Object.fromEntries(services.map(item=>[item.id,{enabled:Boolean(item.defaultEnabled),endpoint:item.defaultEndpoint||'http://127.0.0.1:8020',accountLabel:'',status:item.defaultStatus||'Disconnected'}]));}
|
|
8
|
+
function ready(element,event){return element.ready?Promise.resolve(element):new Promise(resolve=>element.addEventListener(event,()=>resolve(element),{once:true}));}
|
|
9
|
+
|
|
10
|
+
export default class CommunicationAppController{
|
|
11
|
+
constructor({appId,services,channels,labels={},inspectMessage=null,prepareMessageInspection=null,onAdvisoryAction=null}){this.appId=appId;this.services=services;this.channels=channels;this.labels=labels;this.inspectMessage=typeof inspectMessage==='function'?inspectMessage:null;this.prepareMessageInspection=typeof prepareMessageInspection==='function'?prepareMessageInspection:null;this.onAdvisoryAction=typeof onAdvisoryAction==='function'?onAdvisoryAction:null;this.selectionVersion=0;this.preferences=new CommunicationPreferences(appId);this.values={};this.hub=null;this.active=null;this.elements={inbox:document.querySelector('#inbox'),conversation:document.querySelector('#conversation'),settings:document.querySelector('#integrationSettings'),panel:document.querySelector('#settingsPanel'),status:document.querySelector('#appStatus')};}
|
|
12
|
+
async start(){
|
|
13
|
+
loadAndApplyTheme().catch(()=>{});await Promise.all([ready(this.elements.inbox,'unified-inbox-ready'),ready(this.elements.conversation,'conversation-view-ready'),ready(this.elements.settings,'integration-settings-ready')]);
|
|
14
|
+
this.values=await this.preferences.load(defaults(this.services));this.bind();this.configure();await this.refresh();
|
|
15
|
+
}
|
|
16
|
+
bind(){
|
|
17
|
+
document.querySelector('#openSettings').addEventListener('click',()=>this.openSettings());
|
|
18
|
+
this.elements.settings.addEventListener('integration-settings-close',()=>this.closeSettings());
|
|
19
|
+
this.elements.settings.addEventListener('integration-settings-save',event=>this.saveSettings(event.detail.values));
|
|
20
|
+
this.elements.settings.addEventListener('integration-action',event=>this.action(event.detail.service));
|
|
21
|
+
this.elements.inbox.addEventListener('inbox-refresh',()=>this.refresh());
|
|
22
|
+
this.elements.inbox.addEventListener('thread-select',event=>this.select(event.detail.thread));
|
|
23
|
+
this.elements.conversation.addEventListener('communication-send',event=>this.send(event.detail));
|
|
24
|
+
this.elements.conversation.addEventListener('communication-advisory-action',event=>this.onAdvisoryAction?.(event.detail));
|
|
25
|
+
this.elements.panel.addEventListener('click',event=>{if(event.target===this.elements.panel)this.closeSettings()});
|
|
26
|
+
document.addEventListener('keydown',event=>{if(event.key==='Escape'&&!this.elements.panel.hidden)this.closeSettings()});
|
|
27
|
+
}
|
|
28
|
+
configure(){
|
|
29
|
+
const providers=this.services.map(item=>({id:item.id,label:item.label}));
|
|
30
|
+
this.elements.inbox.configure({channels:this.channels,providers,threads:[]});
|
|
31
|
+
this.elements.settings.configure({title:this.labels.settingsTitle||'Connected services',description:this.labels.settingsDescription||'Choose which services appear in this application.',services:this.services,values:this.values});
|
|
32
|
+
this.rebuildHub();
|
|
33
|
+
}
|
|
34
|
+
rebuildHub(){const enabled=this.services.filter(item=>this.values[item.id]?.enabled&&item.unified!==false);const providers=enabled.map(item=>typeof item.providerFactory==='function'?item.providerFactory({service:item,value:this.values[item.id]}):new ArcaneCommunicationBridge({id:item.id,label:item.label,channels:item.channels,endpoint:this.values[item.id].endpoint||item.defaultEndpoint}));this.hub=new CommunicationHub({providers,enabledProviderIds:enabled.map(item=>item.id)});}
|
|
35
|
+
setStatus(message,tone='muted'){this.elements.status.textContent=String(message||'');this.elements.status.dataset.tone=tone;}
|
|
36
|
+
async refresh(){this.elements.inbox.setLoading(true);this.setStatus('Refreshing…');try{const {threads,errors}=await this.hub.refresh();this.elements.inbox.setThreads(threads);if(errors.length)this.setStatus(`${threads.length} conversations · ${errors.length} service${errors.length===1?'':'s'} need attention`,'warning');else this.setStatus(`${threads.length} conversation${threads.length===1?'':'s'}`,'success');}catch(error){this.setStatus(error.message,'error');}finally{this.elements.inbox.setLoading(false)}}
|
|
37
|
+
async select(thread){const selection=++this.selectionVersion;this.active=thread;this.elements.inbox.setActive(thread.id);this.elements.conversation.setStatus('Loading…');try{const messages=await this.hub.messages(thread);let inspection;try{inspection=await inspectMessageRecords(messages,(message,context)=>this.inspectMessage?.(message,thread,context),{prepare:this.prepareMessageInspection?records=>this.prepareMessageInspection(records,thread):null});}catch{inspection=unavailableMessageInspection(messages);}if(selection!==this.selectionVersion)return;const {advisories,failures}=inspection;this.elements.conversation.setConversation(thread,messages,{advisories});const warnings=advisories.size-failures;this.elements.conversation.setStatus(failures?`${failures} message safety check${failures===1?' is':'s are'} unavailable. Review manually before replying.`:warnings?`${warnings} message${warnings===1?' has':'s have'} a safety warning. Review before replying.`:'');}catch(error){if(selection!==this.selectionVersion)return;this.elements.conversation.setConversation(thread,[]);this.elements.conversation.setStatus(error.message);}}
|
|
38
|
+
async send({thread,body}){this.elements.conversation.setBusy(true);this.elements.conversation.setStatus('Sending…');try{await this.hub.send({providerId:thread.providerId,threadId:thread.id,channel:thread.channel,body});this.elements.conversation.clearComposer();await this.select(thread);const service=this.services.find(item=>item.id===thread.providerId);this.elements.conversation.setStatus(service?.simulated?'Simulated locally. Nothing left this device.':'Sent.');}catch(error){this.elements.conversation.setStatus(error.message);}finally{this.elements.conversation.setBusy(false)}}
|
|
39
|
+
openSettings(){this.elements.settings.configure({title:this.labels.settingsTitle,description:this.labels.settingsDescription,services:this.services,values:this.values});this.elements.panel.hidden=false;document.body.classList.add('modal-open');}
|
|
40
|
+
closeSettings(){this.elements.panel.hidden=true;document.body.classList.remove('modal-open');}
|
|
41
|
+
async saveSettings(values){try{this.values=await this.preferences.save(values);this.rebuildHub();this.elements.settings.setStatus('Services saved.');this.closeSettings();await this.refresh();}catch(error){this.elements.settings.setStatus(error.message)}}
|
|
42
|
+
action(service){if(service.externalUrl){const opened=globalThis.open(service.externalUrl,'_blank','noopener,noreferrer');if(!opened)this.elements.settings.setStatus(`Open ${service.externalUrl} to continue.`);return}this.elements.settings.setStatus(`${service.label} connects through the Arcane communications bridge.`)}
|
|
43
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import CommunicationMessage from '../entities/CommunicationMessage.js';
|
|
2
|
+
import CommunicationThread from '../entities/CommunicationThread.js';
|
|
3
|
+
import CommunicationProviderRegistry from './CommunicationProviderRegistry.js?v=2';
|
|
4
|
+
|
|
5
|
+
export default class CommunicationHub extends EventTarget{
|
|
6
|
+
constructor({providers=[],enabledProviderIds=[]}={}){super();this.registry=providers instanceof CommunicationProviderRegistry?providers:new CommunicationProviderRegistry(providers);this.enabled=new Set(enabledProviderIds);this.threads=[];}
|
|
7
|
+
setEnabled(ids=[]){this.enabled=new Set(Array.from(ids,String));return this;}
|
|
8
|
+
enabledProviders(){return this.registry.list().filter(provider=>this.enabled.has(provider.id));}
|
|
9
|
+
async refresh(){
|
|
10
|
+
const results=await Promise.allSettled(this.enabledProviders().map(async provider=>(await provider.listThreads()).map(value=>value instanceof CommunicationThread?value:new CommunicationThread({...value,providerId:provider.id}))));
|
|
11
|
+
const errors=[];const threads=[];for(const result of results){if(result.status==='fulfilled') threads.push(...result.value);else errors.push(result.reason);}
|
|
12
|
+
this.threads=threads.sort((a,b)=>b.updatedAt.localeCompare(a.updatedAt));this.dispatchEvent(new CustomEvent('communications-refresh',{detail:{threads:[...this.threads],errors}}));return {threads:[...this.threads],errors};
|
|
13
|
+
}
|
|
14
|
+
async messages(thread){const record=thread instanceof CommunicationThread?thread:new CommunicationThread(thread);const values=await this.registry.get(record.providerId).getMessages(record.id);return values.map(value=>value instanceof CommunicationMessage?value:new CommunicationMessage({...value,threadId:record.id,providerId:record.providerId,channel:record.channel})).sort((a,b)=>a.timestamp.localeCompare(b.timestamp));}
|
|
15
|
+
async send({providerId,threadId,channel,body,subject='',recipients=[]}={}){if(!String(body||'').trim()) throw new TypeError('A message body is required.');return this.registry.get(providerId).send({threadId,channel,body:String(body),subject:String(subject),recipients:Array.from(recipients||[])});}
|
|
16
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {resolveApplicationLocalStorageKey} from './AppDataScope.js';
|
|
2
|
+
|
|
3
|
+
function adapter(){
|
|
4
|
+
const native=globalThis.Arcane?.preferences;
|
|
5
|
+
if(native?.get&&native?.set) return native;
|
|
6
|
+
return {async get(key){const value=globalThis.localStorage?.getItem(resolveApplicationLocalStorageKey(key));return {found:value!=null,value:value==null?null:JSON.parse(value)}},async set(key,value){globalThis.localStorage?.setItem(resolveApplicationLocalStorageKey(key),JSON.stringify(value));return {key,value}}};
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default class CommunicationPreferences{
|
|
10
|
+
constructor(namespace='communications'){this.key=`arcane.communications.${String(namespace)}`;this.adapter=adapter();}
|
|
11
|
+
async load(defaults={}){const result=await this.adapter.get(this.key);return result?.found&&result.value&&typeof result.value==='object'?{...defaults,...result.value}:{...defaults};}
|
|
12
|
+
async save(values={}){const safe=Object.fromEntries(Object.entries(values).map(([id,value])=>[id,{enabled:Boolean(value?.enabled),endpoint:String(value?.endpoint||''),accountLabel:String(value?.accountLabel||''),status:String(value?.status||'Disconnected')} ]));await this.adapter.set(this.key,safe);return safe;}
|
|
13
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const PROVIDER_ID=/^[a-z0-9][a-z0-9._-]{1,63}$/;
|
|
2
|
+
|
|
3
|
+
export default class CommunicationProviderRegistry{
|
|
4
|
+
constructor(providers=[]){this.providers=new Map();for(const provider of providers) this.register(provider);}
|
|
5
|
+
register(provider){
|
|
6
|
+
const id=String(provider?.id||'').trim().toLowerCase();
|
|
7
|
+
if(!PROVIDER_ID.test(id)) throw new TypeError('Communication provider id is invalid.');
|
|
8
|
+
for(const method of ['listThreads','getMessages','send']) if(typeof provider[method]!=='function') throw new TypeError(`Provider ${id} must implement ${method}().`);
|
|
9
|
+
if(this.providers.has(id)) throw new RangeError(`Communication provider already registered: ${id}`);
|
|
10
|
+
this.providers.set(id,{...provider,id,label:String(provider.label||id),channels:Object.freeze(Array.from(provider.channels||['other'])),listThreads:provider.listThreads.bind(provider),getMessages:provider.getMessages.bind(provider),send:provider.send.bind(provider)});
|
|
11
|
+
return this.get(id);
|
|
12
|
+
}
|
|
13
|
+
get(id){const provider=this.providers.get(String(id||'').toLowerCase());if(!provider) throw new RangeError(`Unknown communication provider: ${id}`);return provider;}
|
|
14
|
+
has(id){return this.providers.has(String(id||'').toLowerCase());}
|
|
15
|
+
list(){return Array.from(this.providers.values());}
|
|
16
|
+
}
|