@holoscript/plugin-civil-engineering 2.0.1

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/src/runtime.ts ADDED
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Runtime integration for @holoscript/plugin-civil-engineering.
3
+ *
4
+ * Bridges the previously dead-wired `dsm_frame_2d` trait into a behavioral
5
+ * TraitHandler that the HoloScript runtime actually dispatches
6
+ * (HoloScriptRuntime.registerTrait -> applyDirectives / updateTraits).
7
+ *
8
+ * Before this module the plugin declared the trait NAME only
9
+ * (pluginMeta.traits) and exported the Direct-Stiffness-Method solver
10
+ * (`solveFrame2D`, `validateFrame2DModel`, `buildFrame2DReceipt`), but nothing
11
+ * invoked the solver THROUGH the runtime — the trait was built-but-dead-wired.
12
+ * This mirrors government-civic-plugin's reference integration (`civic_decision`
13
+ * -> mcdaAnalysis): it wires the deterministic DSM 2D frame solver
14
+ * (`solveFrame2D`) behind the `dsm_frame_2d` trait so the runtime's directive
15
+ * dispatch can run it.
16
+ */
17
+ import { registerPluginTraits } from '@holoscript/core/runtime';
18
+ import {
19
+ solveFrame2D,
20
+ validateFrame2DModel,
21
+ type Frame2DModel,
22
+ type Frame2DResult,
23
+ } from './frame2d';
24
+
25
+ /** Stable id for this plugin's trait ownership tagging. */
26
+ export const CIVIL_ENGINEERING_PLUGIN_ID = 'civil-engineering' as const;
27
+
28
+ /**
29
+ * Config carried by an orb's `@dsm_frame_2d` trait directive. Either supply a
30
+ * fully-formed `model`, or the `Frame2DModel` fields inline (the inline fields
31
+ * are assembled into a model). A missing/invalid model emits
32
+ * `dsm_frame_2d_error`.
33
+ */
34
+ export interface DsmFrame2dTraitConfig {
35
+ /** Complete frame model to solve. Preferred form. */
36
+ model?: Frame2DModel;
37
+ /** Inline model id (used when `model` is omitted). */
38
+ id?: Frame2DModel['id'];
39
+ /** Inline nodes (used when `model` is omitted). */
40
+ nodes?: Frame2DModel['nodes'];
41
+ /** Inline elements (used when `model` is omitted). */
42
+ elements?: Frame2DModel['elements'];
43
+ /** Inline supports (used when `model` is omitted). */
44
+ supports?: Frame2DModel['supports'];
45
+ /** Inline nodal loads (used when `model` is omitted). */
46
+ nodalLoads?: Frame2DModel['nodalLoads'];
47
+ /** Inline distributed loads (used when `model` is omitted). */
48
+ distributedLoads?: Frame2DModel['distributedLoads'];
49
+ }
50
+
51
+ /** Summary payload emitted on `dsm_frame_2d_solved`. */
52
+ export interface DsmFrame2dSolvedEvent {
53
+ nodeId: string;
54
+ /** Solver convergence flag. */
55
+ converged: boolean;
56
+ /** True if all element utilisation ratios < 1.0. */
57
+ structurallyAdequate: boolean;
58
+ /** Maximum absolute nodal displacement (m). */
59
+ maxDisplacementM: number;
60
+ /** Maximum element utilisation ratio. */
61
+ maxUtilisationRatio: number;
62
+ /** Per-node displacements (ux, uy, θz) recovered by the solver. */
63
+ nodeDisplacements: Frame2DResult['nodeDisplacements'];
64
+ /** Support reactions recovered by the solver. */
65
+ reactions: Frame2DResult['reactions'];
66
+ /** Node count in the solved model. */
67
+ nodeCount: number;
68
+ /** Element count in the solved model. */
69
+ elementCount: number;
70
+ }
71
+
72
+ /**
73
+ * Structural view of the runtime trait-handler contract. Matches
74
+ * `@holoscript/core` TraitTypes.TraitHandler at the call sites the runtime
75
+ * actually uses (onAttach / onUpdate receive the node, the directive config,
76
+ * and a context exposing `emit`). Declared locally so the plugin stays
77
+ * decoupled from core's full trait surface.
78
+ */
79
+ export interface TraitDispatchContext {
80
+ emit: (event: string, payload?: unknown) => void;
81
+ setState?: (updates: Record<string, unknown>) => void;
82
+ }
83
+
84
+ export interface RuntimeTraitHandler {
85
+ name: string;
86
+ onAttach?: (node: unknown, config: DsmFrame2dTraitConfig, context: TraitDispatchContext) => void;
87
+ onUpdate?: (
88
+ node: unknown,
89
+ config: DsmFrame2dTraitConfig,
90
+ context: TraitDispatchContext,
91
+ delta: number,
92
+ ) => void;
93
+ }
94
+
95
+ interface DsmFrame2dNode {
96
+ id?: string;
97
+ name?: string;
98
+ properties?: Record<string, unknown>;
99
+ __frame2dResult?: Frame2DResult;
100
+ }
101
+
102
+ /** Resolve a `Frame2DModel` from the directive config (explicit model or inline fields). */
103
+ function resolveModel(config: DsmFrame2dTraitConfig | undefined): Frame2DModel | undefined {
104
+ if (!config) return undefined;
105
+ if (config.model) return config.model;
106
+ if (config.nodes && config.elements && config.supports) {
107
+ return {
108
+ id: config.id ?? 'dsm_frame_2d',
109
+ nodes: config.nodes,
110
+ elements: config.elements,
111
+ supports: config.supports,
112
+ nodalLoads: config.nodalLoads,
113
+ distributedLoads: config.distributedLoads,
114
+ };
115
+ }
116
+ return undefined;
117
+ }
118
+
119
+ /** Run the DSM solver on the directive config, write the result onto the node, and emit. */
120
+ function solveOntoNode(
121
+ node: unknown,
122
+ config: DsmFrame2dTraitConfig | undefined,
123
+ context: TraitDispatchContext,
124
+ ): void {
125
+ const carrier = node as DsmFrame2dNode;
126
+ const nodeId = carrier.id ?? carrier.name ?? 'unknown';
127
+
128
+ const model = resolveModel(config);
129
+ if (!model) {
130
+ context.emit('dsm_frame_2d_error', {
131
+ nodeId,
132
+ error:
133
+ 'dsm_frame_2d trait requires config.model (Frame2DModel) or inline config.nodes + config.elements + config.supports',
134
+ });
135
+ return;
136
+ }
137
+
138
+ // Validate BEFORE solving so a malformed/under-constrained model surfaces a
139
+ // descriptive error instead of a bare "singular stiffness matrix" throw.
140
+ const validation = validateFrame2DModel(model);
141
+ if (!validation.valid) {
142
+ context.emit('dsm_frame_2d_error', {
143
+ nodeId,
144
+ error: `invalid frame model: ${validation.errors.join('; ')}`,
145
+ errors: validation.errors,
146
+ });
147
+ return;
148
+ }
149
+
150
+ try {
151
+ const result = solveFrame2D(model);
152
+ carrier.__frame2dResult = result;
153
+ carrier.properties = {
154
+ ...(carrier.properties ?? {}),
155
+ frame2dConverged: result.converged,
156
+ frame2dStructurallyAdequate: result.structurallyAdequate,
157
+ frame2dMaxDisplacementM: result.maxDisplacementM,
158
+ frame2dMaxUtilisationRatio: result.maxUtilisationRatio,
159
+ };
160
+ const summary: DsmFrame2dSolvedEvent = {
161
+ nodeId,
162
+ converged: result.converged,
163
+ structurallyAdequate: result.structurallyAdequate,
164
+ maxDisplacementM: result.maxDisplacementM,
165
+ maxUtilisationRatio: result.maxUtilisationRatio,
166
+ nodeDisplacements: result.nodeDisplacements,
167
+ reactions: result.reactions,
168
+ nodeCount: model.nodes.length,
169
+ elementCount: model.elements.length,
170
+ };
171
+ context.setState?.({ [`dsm_frame_2d:${nodeId}`]: summary });
172
+ context.emit('dsm_frame_2d_solved', summary);
173
+ } catch (error) {
174
+ context.emit('dsm_frame_2d_error', {
175
+ nodeId,
176
+ error: error instanceof Error ? error.message : String(error),
177
+ });
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Behavioral handler for the civil-engineering `dsm_frame_2d` trait. Runs the
183
+ * deterministic Direct-Stiffness-Method 2D frame solver whenever an orb carrying
184
+ * the trait is attached (and on each per-frame update), writing the result onto
185
+ * the node and emitting `dsm_frame_2d_solved` / `dsm_frame_2d_error`.
186
+ */
187
+ export const dsmFrame2dHandler: RuntimeTraitHandler = {
188
+ name: 'dsm_frame_2d',
189
+ onAttach: (node, config, context) => solveOntoNode(node, config, context),
190
+ onUpdate: (node, config, context) => solveOntoNode(node, config, context),
191
+ };
192
+
193
+ /** A runtime that can register behavioral trait handlers. */
194
+ export interface TraitRegistrar {
195
+ registerTrait(name: string, handler: unknown): void;
196
+ }
197
+
198
+ /**
199
+ * Register civil-engineering behavioral trait handlers into a runtime that
200
+ * exposes `registerTrait(name, handler)` — e.g. `@holoscript/core`
201
+ * HoloScriptRuntime. This is the consumption path the dead-wired tier was
202
+ * missing: after this call the runtime's directive dispatch (applyDirectives /
203
+ * updateTraits) will invoke the DSM frame solver for `@dsm_frame_2d` orbs.
204
+ */
205
+ export function registerCivilEngineeringTraitHandlers(registrar: TraitRegistrar): void {
206
+ registerPluginTraits(registrar, CIVIL_ENGINEERING_PLUGIN_ID, [dsmFrame2dHandler]);
207
+ }
@@ -0,0 +1,25 @@
1
+ /** @load_bearing Trait — Load path and capacity tracking. @trait load_bearing */
2
+ import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
3
+
4
+ export type LoadType = 'dead' | 'live' | 'wind' | 'seismic' | 'snow' | 'impact' | 'thermal';
5
+ export interface LoadCase { id: string; type: LoadType; magnitudeKN: number; direction: [number, number, number]; combinationFactor: number; }
6
+ export interface LoadBearingConfig { capacityKN: number; loadCases: LoadCase[]; redundancyFactor: number; }
7
+ export interface LoadBearingState { totalAppliedKN: number; remainingCapacityKN: number; criticalLoadCase: string | null; isOverloaded: boolean; }
8
+
9
+ const defaultConfig: LoadBearingConfig = { capacityKN: 100, loadCases: [], redundancyFactor: 1.0 };
10
+
11
+ export function createLoadBearingHandler(): TraitHandler<LoadBearingConfig> {
12
+ return { name: 'load_bearing', defaultConfig,
13
+ onAttach(n: HSPlusNode, c: LoadBearingConfig, ctx: TraitContext) {
14
+ const total = c.loadCases.reduce((sum, lc) => sum + lc.magnitudeKN * lc.combinationFactor, 0);
15
+ n.__loadState = { totalAppliedKN: total, remainingCapacityKN: c.capacityKN - total, criticalLoadCase: null, isOverloaded: total > c.capacityKN };
16
+ ctx.emit?.('load:assessed', { total, capacity: c.capacityKN });
17
+ },
18
+ onDetach(n: HSPlusNode, _c: LoadBearingConfig, ctx: TraitContext) { delete n.__loadState; ctx.emit?.('load:removed'); },
19
+ onUpdate() {},
20
+ onEvent(n: HSPlusNode, c: LoadBearingConfig, ctx: TraitContext, e: TraitEvent) {
21
+ const s = n.__loadState as LoadBearingState | undefined; if (!s) return;
22
+ if (e.type === 'load:add') { const kn = (e.payload?.magnitudeKN as number) ?? 0; s.totalAppliedKN += kn; s.remainingCapacityKN = c.capacityKN - s.totalAppliedKN; s.isOverloaded = s.totalAppliedKN > c.capacityKN; if (s.isOverloaded) ctx.emit?.('load:overloaded', { applied: s.totalAppliedKN, capacity: c.capacityKN }); }
23
+ },
24
+ };
25
+ }
@@ -0,0 +1,28 @@
1
+ /** @material_fatigue Trait — Fatigue life estimation. @trait material_fatigue */
2
+ import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
3
+
4
+ export interface MaterialFatigueConfig { sNcurveSlope: number; enduranceLimitMPa: number; designLifeCycles: number; stressRatio: number; surfaceFinishFactor: number; }
5
+ export interface MaterialFatigueState { accumulatedCycles: number; damageFraction: number; remainingLifePercent: number; isFailed: boolean; }
6
+
7
+ const defaultConfig: MaterialFatigueConfig = { sNcurveSlope: -0.1, enduranceLimitMPa: 200, designLifeCycles: 1e6, stressRatio: -1, surfaceFinishFactor: 0.9 };
8
+
9
+ export function createMaterialFatigueHandler(): TraitHandler<MaterialFatigueConfig> {
10
+ return { name: 'material_fatigue', defaultConfig,
11
+ onAttach(n: HSPlusNode, _c: MaterialFatigueConfig, ctx: TraitContext) { n.__fatigueState = { accumulatedCycles: 0, damageFraction: 0, remainingLifePercent: 100, isFailed: false }; ctx.emit?.('fatigue:monitoring_started'); },
12
+ onDetach(n: HSPlusNode, _c: MaterialFatigueConfig, ctx: TraitContext) { delete n.__fatigueState; ctx.emit?.('fatigue:monitoring_stopped'); },
13
+ onUpdate() {},
14
+ onEvent(n: HSPlusNode, c: MaterialFatigueConfig, ctx: TraitContext, e: TraitEvent) {
15
+ const s = n.__fatigueState as MaterialFatigueState | undefined; if (!s || s.isFailed) return;
16
+ if (e.type === 'fatigue:add_cycles') {
17
+ const cycles = (e.payload?.cycles as number) ?? 0;
18
+ const stressMPa = (e.payload?.stressAmplitudeMPa as number) ?? 0;
19
+ s.accumulatedCycles += cycles;
20
+ const nf = stressMPa > c.enduranceLimitMPa ? Math.pow(stressMPa / c.enduranceLimitMPa, 1 / c.sNcurveSlope) * c.designLifeCycles : Infinity;
21
+ s.damageFraction += cycles / nf;
22
+ s.remainingLifePercent = Math.max(0, (1 - s.damageFraction) * 100);
23
+ if (s.damageFraction >= 1) { s.isFailed = true; ctx.emit?.('fatigue:failure', { cycles: s.accumulatedCycles }); }
24
+ else ctx.emit?.('fatigue:updated', { damage: s.damageFraction, remaining: s.remainingLifePercent });
25
+ }
26
+ },
27
+ };
28
+ }
@@ -0,0 +1,28 @@
1
+ /** @structural_analysis Trait — Finite element structural analysis. @trait structural_analysis */
2
+ import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
3
+
4
+ export type StructureType = 'beam' | 'column' | 'slab' | 'truss' | 'frame' | 'shell' | 'foundation';
5
+ export type MaterialType = 'steel' | 'concrete' | 'timber' | 'masonry' | 'composite' | 'aluminum';
6
+ export interface StructuralAnalysisConfig { structureType: StructureType; material: MaterialType; yieldStrengthMPa: number; elasticModulusGPa: number; safetyFactor: number; loadCasesCount: number; }
7
+ export interface StructuralAnalysisState { maxStressMPa: number; maxDeflectionMm: number; utilizationRatio: number; isPasssing: boolean; }
8
+
9
+ const defaultConfig: StructuralAnalysisConfig = { structureType: 'beam', material: 'steel', yieldStrengthMPa: 250, elasticModulusGPa: 200, safetyFactor: 1.5, loadCasesCount: 1 };
10
+
11
+ export function createStructuralAnalysisHandler(): TraitHandler<StructuralAnalysisConfig> {
12
+ return { name: 'structural_analysis', defaultConfig,
13
+ onAttach(n: HSPlusNode, _c: StructuralAnalysisConfig, ctx: TraitContext) { n.__structState = { maxStressMPa: 0, maxDeflectionMm: 0, utilizationRatio: 0, isPasssing: true }; ctx.emit?.('structural:ready'); },
14
+ onDetach(n: HSPlusNode, _c: StructuralAnalysisConfig, ctx: TraitContext) { delete n.__structState; ctx.emit?.('structural:removed'); },
15
+ onUpdate() {},
16
+ onEvent(n: HSPlusNode, c: StructuralAnalysisConfig, ctx: TraitContext, e: TraitEvent) {
17
+ const s = n.__structState as StructuralAnalysisState | undefined; if (!s) return;
18
+ if (e.type === 'structural:analyze') {
19
+ const loadKN = (e.payload?.loadKN as number) ?? 0;
20
+ const spanM = (e.payload?.spanM as number) ?? 1;
21
+ s.maxStressMPa = (loadKN * 1000 * spanM) / (0.001 * c.elasticModulusGPa * 1e9) * 1e6;
22
+ s.utilizationRatio = s.maxStressMPa / (c.yieldStrengthMPa / c.safetyFactor);
23
+ s.isPasssing = s.utilizationRatio <= 1.0;
24
+ ctx.emit?.('structural:result', { stress: s.maxStressMPa, utilization: s.utilizationRatio, pass: s.isPasssing });
25
+ }
26
+ },
27
+ };
28
+ }
@@ -0,0 +1,4 @@
1
+ export interface HSPlusNode { id?: string; properties?: Record<string, unknown>; [key: string]: unknown; }
2
+ export interface TraitContext { emit?: (event: string, payload?: unknown) => void; [key: string]: unknown; }
3
+ export interface TraitEvent { type: string; payload?: Record<string, unknown>; [key: string]: unknown; }
4
+ export interface TraitHandler<T = unknown> { name: string; defaultConfig: T; onAttach(n: HSPlusNode, c: T, ctx: TraitContext): void; onDetach(n: HSPlusNode, c: T, ctx: TraitContext): void; onUpdate(n: HSPlusNode, c: T, ctx: TraitContext, d: number): void; onEvent(n: HSPlusNode, c: T, ctx: TraitContext, e: TraitEvent): void; }
package/tsconfig.json ADDED
@@ -0,0 +1 @@
1
+ { "extends": "../../../tsconfig.json", "compilerOptions": { "outDir": "dist", "rootDir": "src", "declaration": true }, "include": ["src"] }
@@ -0,0 +1,22 @@
1
+ import { defineConfig } from 'vitest/config';
2
+ import { resolve } from 'path';
3
+
4
+ // resolve.alias is REQUIRED: without it, `@holoscript/core/runtime` resolves to
5
+ // the STALE built dist and registerPluginTraits is missing ("not a function").
6
+ // Point the subpath imports at core/engine source so the runtime barrel
7
+ // (core/src/runtime.ts) and shared registrar resolve from source. Mirrors
8
+ // government-civic-plugin/vitest.config.ts.
9
+ export default defineConfig({
10
+ resolve: {
11
+ alias: {
12
+ '@holoscript/engine': resolve(__dirname, '../../engine/src'),
13
+ '@holoscript/core': resolve(__dirname, '../../core/src'),
14
+ },
15
+ },
16
+ test: {
17
+ globals: true,
18
+ environment: 'node',
19
+ include: ['src/**/*.test.ts'],
20
+ passWithNoTests: true,
21
+ },
22
+ });