@holoscript/plugin-government-civic 2.0.1 → 2.0.2
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/package.json +3 -3
- package/src/__tests__/civicsolver.test.ts +29 -25
- package/src/__tests__/government-civic.test.ts +57 -14
- package/src/__tests__/runtime-integration.test.ts +4 -4
- package/src/civicsolver.ts +129 -62
- package/src/index.ts +48 -6
- package/src/runtime.ts +7 -3
- package/src/traits/CivicComplianceTrait.ts +58 -20
- package/src/traits/PermitTrait.ts +31 -6
- package/src/traits/PublicMeetingTrait.ts +37 -8
- package/src/traits/ServiceRequestTrait.ts +29 -5
- package/src/traits/VotingRecordTrait.ts +26 -8
- package/src/traits/types.ts +25 -4
- package/tsconfig.json +10 -1
- package/LICENSE +0 -21
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@holoscript/plugin-government-civic",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "HoloScript domain plugin for government and civic services — permits, public meetings, service requests, voting records, and ADA/FOIA compliance.",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"peerDependencies": {
|
|
7
|
-
"@holoscript/core": "8.0.
|
|
7
|
+
"@holoscript/core": ">=8.0.0"
|
|
8
8
|
},
|
|
9
9
|
"license": "MIT",
|
|
10
10
|
"scripts": {
|
|
11
11
|
"test": "vitest run --passWithNoTests",
|
|
12
12
|
"test:coverage": "vitest run --coverage --passWithNoTests"
|
|
13
13
|
}
|
|
14
|
-
}
|
|
14
|
+
}
|
|
@@ -23,12 +23,12 @@ import {
|
|
|
23
23
|
describe('permitScoring', () => {
|
|
24
24
|
it('compositeScore = weighted sum of criterion scores', () => {
|
|
25
25
|
const criteria = [
|
|
26
|
-
{ name: 'safety',
|
|
27
|
-
{ name: 'zoning',
|
|
28
|
-
{ name: 'env',
|
|
26
|
+
{ name: 'safety', weight: 0.4, score: 80 },
|
|
27
|
+
{ name: 'zoning', weight: 0.35, score: 90 },
|
|
28
|
+
{ name: 'env', weight: 0.25, score: 70 },
|
|
29
29
|
];
|
|
30
30
|
const r = permitScoring(criteria);
|
|
31
|
-
const expected = 0.
|
|
31
|
+
const expected = 0.4 * 80 + 0.35 * 90 + 0.25 * 70;
|
|
32
32
|
expect(r.compositeScore).toBeCloseTo(expected, 4);
|
|
33
33
|
});
|
|
34
34
|
|
|
@@ -46,12 +46,12 @@ describe('permitScoring', () => {
|
|
|
46
46
|
|
|
47
47
|
it('breakdown includes contribution = score × weight', () => {
|
|
48
48
|
const criteria = [
|
|
49
|
-
{ name: 'env', weight: 0.
|
|
50
|
-
{ name: 'traffic', weight: 0.
|
|
49
|
+
{ name: 'env', weight: 0.6, score: 75 },
|
|
50
|
+
{ name: 'traffic', weight: 0.4, score: 50 },
|
|
51
51
|
];
|
|
52
52
|
const r = permitScoring(criteria);
|
|
53
|
-
expect(r.breakdown[0].contribution).toBeCloseTo(0.
|
|
54
|
-
expect(r.breakdown[1].contribution).toBeCloseTo(0.
|
|
53
|
+
expect(r.breakdown[0].contribution).toBeCloseTo(0.6 * 75, 4);
|
|
54
|
+
expect(r.breakdown[1].contribution).toBeCloseTo(0.4 * 50, 4);
|
|
55
55
|
});
|
|
56
56
|
|
|
57
57
|
it('throws when weights do not sum to 1.0', () => {
|
|
@@ -76,14 +76,14 @@ describe('mcdaAnalysis', () => {
|
|
|
76
76
|
{ id: 'project-C', scores: [50, 90, 70] },
|
|
77
77
|
];
|
|
78
78
|
const criteria = [
|
|
79
|
-
{ name: 'cost-effectiveness', weight: 0.
|
|
80
|
-
{ name: 'community-impact',
|
|
81
|
-
{ name: 'implementation-ease',weight: 0.
|
|
79
|
+
{ name: 'cost-effectiveness', weight: 0.5, isBenefit: true },
|
|
80
|
+
{ name: 'community-impact', weight: 0.3, isBenefit: true },
|
|
81
|
+
{ name: 'implementation-ease', weight: 0.2, isBenefit: true },
|
|
82
82
|
];
|
|
83
83
|
|
|
84
84
|
it('winner is a valid candidate id', () => {
|
|
85
85
|
const r = mcdaAnalysis(candidates, criteria);
|
|
86
|
-
const ids = candidates.map(c => c.id);
|
|
86
|
+
const ids = candidates.map((c) => c.id);
|
|
87
87
|
expect(ids).toContain(r.winner);
|
|
88
88
|
});
|
|
89
89
|
|
|
@@ -102,7 +102,7 @@ describe('mcdaAnalysis', () => {
|
|
|
102
102
|
|
|
103
103
|
it('ranks are 1-indexed and sequential', () => {
|
|
104
104
|
const r = mcdaAnalysis(candidates, criteria);
|
|
105
|
-
const ranks = r.ranking.map(x => x.rank).sort((a, b) => a - b);
|
|
105
|
+
const ranks = r.ranking.map((x) => x.rank).sort((a, b) => a - b);
|
|
106
106
|
expect(ranks).toEqual([1, 2, 3]);
|
|
107
107
|
});
|
|
108
108
|
|
|
@@ -190,19 +190,19 @@ describe('votingCohesion', () => {
|
|
|
190
190
|
{ memberId: 'A2', partyId: 'A', vote: 'yes' as const },
|
|
191
191
|
{ memberId: 'A3', partyId: 'A', vote: 'yes' as const },
|
|
192
192
|
{ memberId: 'B1', partyId: 'B', vote: 'yes' as const },
|
|
193
|
-
{ memberId: 'B2', partyId: 'B', vote: 'no'
|
|
194
|
-
{ memberId: 'B3', partyId: 'B', vote: 'no'
|
|
193
|
+
{ memberId: 'B2', partyId: 'B', vote: 'no' as const },
|
|
194
|
+
{ memberId: 'B3', partyId: 'B', vote: 'no' as const },
|
|
195
195
|
];
|
|
196
196
|
|
|
197
197
|
it('unanimous party cohesion = 1.0', () => {
|
|
198
198
|
const r = votingCohesion(votes);
|
|
199
|
-
const partyA = r.partyCohesion.find(p => p.partyId === 'A');
|
|
199
|
+
const partyA = r.partyCohesion.find((p) => p.partyId === 'A');
|
|
200
200
|
expect(partyA!.cohesion).toBeCloseTo(1.0, 4);
|
|
201
201
|
});
|
|
202
202
|
|
|
203
203
|
it('split party cohesion < 1', () => {
|
|
204
204
|
const r = votingCohesion(votes);
|
|
205
|
-
const partyB = r.partyCohesion.find(p => p.partyId === 'B');
|
|
205
|
+
const partyB = r.partyCohesion.find((p) => p.partyId === 'B');
|
|
206
206
|
expect(partyB!.cohesion).toBeGreaterThanOrEqual(0);
|
|
207
207
|
expect(partyB!.cohesion).toBeLessThan(1);
|
|
208
208
|
});
|
|
@@ -219,7 +219,10 @@ describe('votingCohesion', () => {
|
|
|
219
219
|
});
|
|
220
220
|
|
|
221
221
|
it('majority=no when more no than yes', () => {
|
|
222
|
-
const noMajority = votes.map(v => ({
|
|
222
|
+
const noMajority = votes.map((v) => ({
|
|
223
|
+
...v,
|
|
224
|
+
vote: (v.partyId === 'A' ? 'no' : v.vote) as 'yes' | 'no' | 'abstain',
|
|
225
|
+
}));
|
|
223
226
|
const r = votingCohesion(noMajority); // 1 yes, 5 no
|
|
224
227
|
expect(r.majority).toBe('no');
|
|
225
228
|
});
|
|
@@ -252,7 +255,7 @@ describe('polsbyPopper', () => {
|
|
|
252
255
|
// Very elongated: narrow strip, A=1, P=100 → PP = 4π/10000 ≈ 0.00125
|
|
253
256
|
const r = polsbyPopper(1, 100);
|
|
254
257
|
expect(r.classification).toBe('gerrymandered');
|
|
255
|
-
expect(r.compactnessScore).toBeLessThan(0.
|
|
258
|
+
expect(r.compactnessScore).toBeLessThan(0.2);
|
|
256
259
|
});
|
|
257
260
|
|
|
258
261
|
it('moderate classification for 0.20 ≤ score < 0.50', () => {
|
|
@@ -264,9 +267,10 @@ describe('polsbyPopper', () => {
|
|
|
264
267
|
});
|
|
265
268
|
|
|
266
269
|
it('compactnessScore = 4π × area / perimeter²', () => {
|
|
267
|
-
const A = 5,
|
|
270
|
+
const A = 5,
|
|
271
|
+
P = 15;
|
|
268
272
|
const r = polsbyPopper(A, P);
|
|
269
|
-
expect(r.compactnessScore).toBeCloseTo((4 * Math.PI * A) /
|
|
273
|
+
expect(r.compactnessScore).toBeCloseTo((4 * Math.PI * A) / P ** 2, 6);
|
|
270
274
|
});
|
|
271
275
|
|
|
272
276
|
it('throws for non-positive area', () => {
|
|
@@ -278,9 +282,9 @@ describe('polsbyPopper', () => {
|
|
|
278
282
|
|
|
279
283
|
describe('budgetVariance', () => {
|
|
280
284
|
const lines = [
|
|
281
|
-
{ category: 'Roads',
|
|
282
|
-
{ category: 'Parks',
|
|
283
|
-
{ category: 'Admin',
|
|
285
|
+
{ category: 'Roads', budgeted: 100_000, actual: 95_000 }, // under → favorable
|
|
286
|
+
{ category: 'Parks', budgeted: 50_000, actual: 60_000 }, // over → unfavorable
|
|
287
|
+
{ category: 'Admin', budgeted: 75_000, actual: 75_500 }, // near → on-target
|
|
284
288
|
];
|
|
285
289
|
|
|
286
290
|
it('variance = actual - budgeted', () => {
|
|
@@ -306,7 +310,7 @@ describe('budgetVariance', () => {
|
|
|
306
310
|
});
|
|
307
311
|
|
|
308
312
|
it('flaggedLines contains categories with |variance%| > threshold', () => {
|
|
309
|
-
const r = budgetVariance(lines, 0.
|
|
313
|
+
const r = budgetVariance(lines, 0.1); // 10% threshold
|
|
310
314
|
// Parks: 20% over → flagged. Roads: 5% under → not flagged (below 10%)
|
|
311
315
|
expect(r.flaggedLines).toContain('Parks');
|
|
312
316
|
expect(r.flaggedLines).not.toContain('Roads');
|
|
@@ -17,8 +17,12 @@ import type { HSPlusNode, TraitContext } from '../traits/types';
|
|
|
17
17
|
|
|
18
18
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
19
19
|
|
|
20
|
-
function makeNode(): HSPlusNode {
|
|
21
|
-
|
|
20
|
+
function makeNode(): HSPlusNode {
|
|
21
|
+
return { id: 'n1', traits: [], state: {}, position: [0, 0, 0] };
|
|
22
|
+
}
|
|
23
|
+
function makeCtx(): TraitContext {
|
|
24
|
+
return { emit: vi.fn(), scene: {} };
|
|
25
|
+
}
|
|
22
26
|
|
|
23
27
|
// ── Plugin metadata ───────────────────────────────────────────────────────────
|
|
24
28
|
|
|
@@ -77,9 +81,15 @@ describe('PermitTrait', () => {
|
|
|
77
81
|
const node = makeNode();
|
|
78
82
|
const ctx = makeCtx();
|
|
79
83
|
handler.onAttach(node, cfg, ctx);
|
|
80
|
-
handler.onEvent(node, cfg, ctx, {
|
|
84
|
+
handler.onEvent(node, cfg, ctx, {
|
|
85
|
+
type: 'permit:update_status',
|
|
86
|
+
payload: { status: 'approved' },
|
|
87
|
+
});
|
|
81
88
|
expect((node.__permitState as { status: string }).status).toBe('approved');
|
|
82
|
-
expect(ctx.emit).toHaveBeenCalledWith(
|
|
89
|
+
expect(ctx.emit).toHaveBeenCalledWith(
|
|
90
|
+
'permit:status_changed',
|
|
91
|
+
expect.objectContaining({ to: 'approved' })
|
|
92
|
+
);
|
|
83
93
|
});
|
|
84
94
|
|
|
85
95
|
it('cleans up on detach', () => {
|
|
@@ -106,7 +116,13 @@ describe('PublicMeetingTrait', () => {
|
|
|
106
116
|
publicCommentMinutesPerSpeaker: 3,
|
|
107
117
|
agenda: [
|
|
108
118
|
{ id: '1', title: 'Call to Order', type: 'action', durationMinutes: 5, requiresVote: false },
|
|
109
|
-
{
|
|
119
|
+
{
|
|
120
|
+
id: '2',
|
|
121
|
+
title: 'Public Comment',
|
|
122
|
+
type: 'public_comment',
|
|
123
|
+
durationMinutes: 30,
|
|
124
|
+
requiresVote: false,
|
|
125
|
+
},
|
|
110
126
|
],
|
|
111
127
|
accessibilityFeatures: ['captioning'],
|
|
112
128
|
};
|
|
@@ -221,7 +237,10 @@ describe('VotingRecordTrait', () => {
|
|
|
221
237
|
const ctx = makeCtx();
|
|
222
238
|
handler.onAttach(node, cfg, ctx);
|
|
223
239
|
handler.onEvent(node, cfg, ctx, { type: 'vote:open' });
|
|
224
|
-
handler.onEvent(node, cfg, ctx, {
|
|
240
|
+
handler.onEvent(node, cfg, ctx, {
|
|
241
|
+
type: 'vote:cast',
|
|
242
|
+
payload: { memberId: 'CM-A', memberName: 'Councilmember A', vote: 'aye' },
|
|
243
|
+
});
|
|
225
244
|
expect((node.__votingState as { ayeCount: number }).ayeCount).toBe(1);
|
|
226
245
|
});
|
|
227
246
|
|
|
@@ -232,14 +251,23 @@ describe('VotingRecordTrait', () => {
|
|
|
232
251
|
handler.onAttach(node, cfg, ctx);
|
|
233
252
|
handler.onEvent(node, cfg, ctx, { type: 'vote:open' });
|
|
234
253
|
for (const id of ['CM-A', 'CM-B', 'CM-C']) {
|
|
235
|
-
handler.onEvent(node, cfg, ctx, {
|
|
254
|
+
handler.onEvent(node, cfg, ctx, {
|
|
255
|
+
type: 'vote:cast',
|
|
256
|
+
payload: { memberId: id, memberName: id, vote: 'aye' },
|
|
257
|
+
});
|
|
236
258
|
}
|
|
237
259
|
for (const id of ['CM-D', 'Mayor']) {
|
|
238
|
-
handler.onEvent(node, cfg, ctx, {
|
|
260
|
+
handler.onEvent(node, cfg, ctx, {
|
|
261
|
+
type: 'vote:cast',
|
|
262
|
+
payload: { memberId: id, memberName: id, vote: 'nay' },
|
|
263
|
+
});
|
|
239
264
|
}
|
|
240
265
|
handler.onEvent(node, cfg, ctx, { type: 'vote:close' });
|
|
241
266
|
expect((node.__votingState as { outcome: string }).outcome).toBe('passed');
|
|
242
|
-
expect(ctx.emit).toHaveBeenCalledWith(
|
|
267
|
+
expect(ctx.emit).toHaveBeenCalledWith(
|
|
268
|
+
'vote:closed',
|
|
269
|
+
expect.objectContaining({ outcome: 'passed' })
|
|
270
|
+
);
|
|
243
271
|
});
|
|
244
272
|
});
|
|
245
273
|
|
|
@@ -273,9 +301,15 @@ describe('CivicComplianceTrait', () => {
|
|
|
273
301
|
handler.onAttach(node, cfg, ctx);
|
|
274
302
|
const s = node.__complianceState as { checks: Array<{ id: string; status: string }> };
|
|
275
303
|
const checkId = s.checks[0]!.id;
|
|
276
|
-
handler.onEvent(node, cfg, ctx, {
|
|
304
|
+
handler.onEvent(node, cfg, ctx, {
|
|
305
|
+
type: 'compliance:update_check',
|
|
306
|
+
payload: { checkId, status: 'compliant' },
|
|
307
|
+
});
|
|
277
308
|
expect(s.checks[0]!.status).toBe('compliant');
|
|
278
|
-
expect(ctx.emit).toHaveBeenCalledWith(
|
|
309
|
+
expect(ctx.emit).toHaveBeenCalledWith(
|
|
310
|
+
'compliance:check_updated',
|
|
311
|
+
expect.objectContaining({ status: 'compliant' })
|
|
312
|
+
);
|
|
279
313
|
});
|
|
280
314
|
|
|
281
315
|
it('compliance:foia_received creates a pending FOIA request', () => {
|
|
@@ -283,11 +317,17 @@ describe('CivicComplianceTrait', () => {
|
|
|
283
317
|
const node = makeNode();
|
|
284
318
|
const ctx = makeCtx();
|
|
285
319
|
handler.onAttach(node, cfg, ctx);
|
|
286
|
-
handler.onEvent(node, cfg, ctx, {
|
|
320
|
+
handler.onEvent(node, cfg, ctx, {
|
|
321
|
+
type: 'compliance:foia_received',
|
|
322
|
+
payload: { requestId: 'FOIA-001', description: 'Budget docs' },
|
|
323
|
+
});
|
|
287
324
|
const s = node.__complianceState as { foiaRequests: unknown[]; openFoiaCount: number };
|
|
288
325
|
expect(s.foiaRequests).toHaveLength(1);
|
|
289
326
|
expect(s.openFoiaCount).toBe(1);
|
|
290
|
-
expect(ctx.emit).toHaveBeenCalledWith(
|
|
327
|
+
expect(ctx.emit).toHaveBeenCalledWith(
|
|
328
|
+
'compliance:foia_received',
|
|
329
|
+
expect.objectContaining({ requestId: 'FOIA-001' })
|
|
330
|
+
);
|
|
291
331
|
});
|
|
292
332
|
|
|
293
333
|
it('audit log entry is stored', () => {
|
|
@@ -295,7 +335,10 @@ describe('CivicComplianceTrait', () => {
|
|
|
295
335
|
const node = makeNode();
|
|
296
336
|
const ctx = makeCtx();
|
|
297
337
|
handler.onAttach(node, cfg, ctx);
|
|
298
|
-
handler.onEvent(node, cfg, ctx, {
|
|
338
|
+
handler.onEvent(node, cfg, ctx, {
|
|
339
|
+
type: 'compliance:audit_log',
|
|
340
|
+
payload: { action: 'page_view', actor: 'user-99', details: 'Viewed homepage' },
|
|
341
|
+
});
|
|
299
342
|
const s = node.__complianceState as { auditLog: unknown[] };
|
|
300
343
|
expect(s.auditLog).toHaveLength(1);
|
|
301
344
|
});
|
|
@@ -53,7 +53,7 @@ describe('government-civic -> HoloScript runtime integration (civic_decision)',
|
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
await runtime.executeNode(
|
|
56
|
-
civicDecisionOrb({ candidates: TWO_CANDIDATES, criteria: TWO_CANDIDATE_CRITERIA }) as never
|
|
56
|
+
civicDecisionOrb({ candidates: TWO_CANDIDATES, criteria: TWO_CANDIDATE_CRITERIA }) as never
|
|
57
57
|
);
|
|
58
58
|
await flush();
|
|
59
59
|
|
|
@@ -79,7 +79,7 @@ describe('government-civic -> HoloScript runtime integration (civic_decision)',
|
|
|
79
79
|
runtime.on('civic_decision_solved', (e: unknown) => solved.push(e));
|
|
80
80
|
|
|
81
81
|
await runtime.executeNode(
|
|
82
|
-
civicDecisionOrb({ candidates: TWO_CANDIDATES, criteria: TWO_CANDIDATE_CRITERIA }) as never
|
|
82
|
+
civicDecisionOrb({ candidates: TWO_CANDIDATES, criteria: TWO_CANDIDATE_CRITERIA }) as never
|
|
83
83
|
);
|
|
84
84
|
await flush();
|
|
85
85
|
|
|
@@ -91,7 +91,7 @@ describe('government-civic -> HoloScript runtime integration (civic_decision)',
|
|
|
91
91
|
registerGovernmentCivicTraitHandlers(runtime);
|
|
92
92
|
|
|
93
93
|
await runtime.executeNode(
|
|
94
|
-
civicDecisionOrb({ candidates: TWO_CANDIDATES, criteria: TWO_CANDIDATE_CRITERIA }) as never
|
|
94
|
+
civicDecisionOrb({ candidates: TWO_CANDIDATES, criteria: TWO_CANDIDATE_CRITERIA }) as never
|
|
95
95
|
);
|
|
96
96
|
await flush();
|
|
97
97
|
|
|
@@ -118,7 +118,7 @@ describe('government-civic -> HoloScript runtime integration (civic_decision)',
|
|
|
118
118
|
// handler's try/catch turns into a civic_decision_error rather than a throw.
|
|
119
119
|
const mismatched: MCDACandidate[] = [{ id: 'A', scores: [1, 2] }];
|
|
120
120
|
await runtime.executeNode(
|
|
121
|
-
civicDecisionOrb({ candidates: mismatched, criteria: TWO_CANDIDATE_CRITERIA }) as never
|
|
121
|
+
civicDecisionOrb({ candidates: mismatched, criteria: TWO_CANDIDATE_CRITERIA }) as never
|
|
122
122
|
);
|
|
123
123
|
await flush();
|
|
124
124
|
|
package/src/civicsolver.ts
CHANGED
|
@@ -23,8 +23,8 @@ import { buildDomainSimulationReceipt, type DomainSimulationReceipt } from '@hol
|
|
|
23
23
|
|
|
24
24
|
export interface PermitCriteria {
|
|
25
25
|
name: string;
|
|
26
|
-
weight: number;
|
|
27
|
-
score: number;
|
|
26
|
+
weight: number; // sum of all weights should be 1.0
|
|
27
|
+
score: number; // 0–100
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export interface PermitScoringResult {
|
|
@@ -46,7 +46,7 @@ export interface MCDACandidate {
|
|
|
46
46
|
|
|
47
47
|
export interface MCDACriterion {
|
|
48
48
|
name: string;
|
|
49
|
-
weight: number;
|
|
49
|
+
weight: number; // 0–1
|
|
50
50
|
/** true = higher is better (benefit), false = lower is better (cost) */
|
|
51
51
|
isBenefit: boolean;
|
|
52
52
|
}
|
|
@@ -87,7 +87,13 @@ export interface CohesionResult {
|
|
|
87
87
|
/** Overall chamber Rice index (0=perfect split, 1=unanimous) */
|
|
88
88
|
chamberCohesion: number;
|
|
89
89
|
/** Per-party Rice cohesion */
|
|
90
|
-
partyCohesion: Array<{
|
|
90
|
+
partyCohesion: Array<{
|
|
91
|
+
partyId: string;
|
|
92
|
+
cohesion: number;
|
|
93
|
+
yesCount: number;
|
|
94
|
+
noCount: number;
|
|
95
|
+
size: number;
|
|
96
|
+
}>;
|
|
91
97
|
/** Winning coalition */
|
|
92
98
|
majority: 'yes' | 'no' | 'tie';
|
|
93
99
|
}
|
|
@@ -122,7 +128,7 @@ export interface BudgetVarianceResult {
|
|
|
122
128
|
totalActual: number;
|
|
123
129
|
totalVariancePct: number;
|
|
124
130
|
overallStatus: 'favorable' | 'unfavorable' | 'on-target';
|
|
125
|
-
flaggedLines: string[];
|
|
131
|
+
flaggedLines: string[]; // categories with variance > 10%
|
|
126
132
|
}
|
|
127
133
|
|
|
128
134
|
export interface GovtReceiptOptions {
|
|
@@ -131,15 +137,13 @@ export interface GovtReceiptOptions {
|
|
|
131
137
|
|
|
132
138
|
// ─── Permit Scoring ───────────────────────────────────────────────────────────
|
|
133
139
|
|
|
134
|
-
export function permitScoring(
|
|
135
|
-
criteria: PermitCriteria[],
|
|
136
|
-
threshold = 70,
|
|
137
|
-
): PermitScoringResult {
|
|
140
|
+
export function permitScoring(criteria: PermitCriteria[], threshold = 70): PermitScoringResult {
|
|
138
141
|
if (criteria.length === 0) throw new Error('No permit criteria provided');
|
|
139
142
|
const totalWeight = criteria.reduce((s, c) => s + c.weight, 0);
|
|
140
|
-
if (Math.abs(totalWeight - 1.0) > 0.01)
|
|
143
|
+
if (Math.abs(totalWeight - 1.0) > 0.01)
|
|
144
|
+
throw new Error(`Weights must sum to 1.0 (got ${totalWeight.toFixed(3)})`);
|
|
141
145
|
|
|
142
|
-
const breakdown = criteria.map(c => ({
|
|
146
|
+
const breakdown = criteria.map((c) => ({
|
|
143
147
|
name: c.name,
|
|
144
148
|
score: c.score,
|
|
145
149
|
weight: c.weight,
|
|
@@ -155,30 +159,33 @@ export function permitScoring(
|
|
|
155
159
|
|
|
156
160
|
/** Normalize a column to [0,1] range */
|
|
157
161
|
function normalizeColumn(values: number[]): number[] {
|
|
158
|
-
const min = Math.min(...values),
|
|
162
|
+
const min = Math.min(...values),
|
|
163
|
+
max = Math.max(...values);
|
|
159
164
|
if (max === min) return values.map(() => 0.5);
|
|
160
|
-
return values.map(v => (v - min) / (max - min));
|
|
165
|
+
return values.map((v) => (v - min) / (max - min));
|
|
161
166
|
}
|
|
162
167
|
|
|
163
|
-
export function mcdaAnalysis(
|
|
164
|
-
candidates: MCDACandidate[],
|
|
165
|
-
criteria: MCDACriterion[],
|
|
166
|
-
): MCDAResult {
|
|
168
|
+
export function mcdaAnalysis(candidates: MCDACandidate[], criteria: MCDACriterion[]): MCDAResult {
|
|
167
169
|
if (candidates.length === 0) throw new Error('No MCDA candidates');
|
|
168
170
|
if (criteria.length === 0) throw new Error('No MCDA criteria');
|
|
169
|
-
if (candidates.some(c => c.scores.length !== criteria.length))
|
|
171
|
+
if (candidates.some((c) => c.scores.length !== criteria.length))
|
|
172
|
+
throw new Error('scores.length must match criteria.length');
|
|
170
173
|
|
|
171
|
-
const n = candidates.length,
|
|
174
|
+
const n = candidates.length,
|
|
175
|
+
m = criteria.length;
|
|
172
176
|
|
|
173
177
|
// Weighted sum score
|
|
174
|
-
const weightedScores = candidates.map(c =>
|
|
175
|
-
c.scores.reduce(
|
|
178
|
+
const weightedScores = candidates.map((c) =>
|
|
179
|
+
c.scores.reduce(
|
|
180
|
+
(acc, s, j) => acc + s * criteria[j].weight * (criteria[j].isBenefit ? 1 : -1),
|
|
181
|
+
0
|
|
182
|
+
)
|
|
176
183
|
);
|
|
177
184
|
|
|
178
185
|
// TOPSIS: normalize → weight → ideal/anti-ideal → distance
|
|
179
186
|
const normalizedMatrix: number[][] = Array.from({ length: n }, () => Array(m).fill(0));
|
|
180
187
|
for (let j = 0; j < m; j++) {
|
|
181
|
-
const col = candidates.map(c => c.scores[j]);
|
|
188
|
+
const col = candidates.map((c) => c.scores[j]);
|
|
182
189
|
const norm = normalizeColumn(col);
|
|
183
190
|
for (let i = 0; i < n; i++) {
|
|
184
191
|
normalizedMatrix[i][j] = norm[i] * criteria[j].weight;
|
|
@@ -186,10 +193,18 @@ export function mcdaAnalysis(
|
|
|
186
193
|
}
|
|
187
194
|
|
|
188
195
|
// Ideal best/worst per criterion
|
|
189
|
-
const ideal
|
|
190
|
-
|
|
196
|
+
const ideal = criteria.map((cr, j) =>
|
|
197
|
+
cr.isBenefit
|
|
198
|
+
? Math.max(...normalizedMatrix.map((r) => r[j]))
|
|
199
|
+
: Math.min(...normalizedMatrix.map((r) => r[j]))
|
|
200
|
+
);
|
|
201
|
+
const antiIdeal = criteria.map((cr, j) =>
|
|
202
|
+
cr.isBenefit
|
|
203
|
+
? Math.min(...normalizedMatrix.map((r) => r[j]))
|
|
204
|
+
: Math.max(...normalizedMatrix.map((r) => r[j]))
|
|
205
|
+
);
|
|
191
206
|
|
|
192
|
-
const topsisScores = normalizedMatrix.map(row => {
|
|
207
|
+
const topsisScores = normalizedMatrix.map((row) => {
|
|
193
208
|
const dPos = Math.sqrt(row.reduce((acc, v, j) => acc + (v - ideal[j]) ** 2, 0));
|
|
194
209
|
const dNeg = Math.sqrt(row.reduce((acc, v, j) => acc + (v - antiIdeal[j]) ** 2, 0));
|
|
195
210
|
return dNeg / (dPos + dNeg + 1e-15);
|
|
@@ -211,27 +226,40 @@ export function quorumCalculator(
|
|
|
211
226
|
yesVotes: number,
|
|
212
227
|
noVotes: number,
|
|
213
228
|
abstentions: number,
|
|
214
|
-
quorumType: QuorumType = 'simple'
|
|
229
|
+
quorumType: QuorumType = 'simple'
|
|
215
230
|
): QuorumResult {
|
|
216
231
|
if (totalMembers < 1) throw new Error('totalMembers must be ≥ 1');
|
|
217
232
|
if (presentMembers > totalMembers) throw new Error('presentMembers cannot exceed totalMembers');
|
|
218
|
-
if (yesVotes + noVotes + abstentions > presentMembers)
|
|
233
|
+
if (yesVotes + noVotes + abstentions > presentMembers)
|
|
234
|
+
throw new Error('Votes exceed present members');
|
|
219
235
|
|
|
220
236
|
const quorumRequired = Math.floor(totalMembers / 2) + 1; // strict majority
|
|
221
237
|
const quorumMet = presentMembers >= quorumRequired;
|
|
222
238
|
|
|
223
239
|
const passThreshold =
|
|
224
|
-
quorumType === 'supermajority'
|
|
225
|
-
|
|
226
|
-
|
|
240
|
+
quorumType === 'supermajority'
|
|
241
|
+
? 2 / 3
|
|
242
|
+
: quorumType === 'absolute'
|
|
243
|
+
? (totalMembers / 2 + 1) / totalMembers
|
|
244
|
+
: 0.5; // simple majority of votes cast
|
|
227
245
|
|
|
228
246
|
const votingBase = quorumType === 'absolute' ? totalMembers : yesVotes + noVotes;
|
|
229
|
-
const motionPassed = quorumMet && votingBase > 0 &&
|
|
247
|
+
const motionPassed = quorumMet && votingBase > 0 && yesVotes / votingBase > passThreshold;
|
|
230
248
|
const marginVotes = motionPassed
|
|
231
249
|
? yesVotes - Math.ceil((yesVotes + noVotes) * passThreshold)
|
|
232
250
|
: Math.ceil((yesVotes + noVotes) * passThreshold) - yesVotes;
|
|
233
251
|
|
|
234
|
-
return {
|
|
252
|
+
return {
|
|
253
|
+
totalMembers,
|
|
254
|
+
presentMembers,
|
|
255
|
+
yesVotes,
|
|
256
|
+
noVotes,
|
|
257
|
+
abstentions,
|
|
258
|
+
quorumMet,
|
|
259
|
+
passThreshold,
|
|
260
|
+
motionPassed,
|
|
261
|
+
marginVotes,
|
|
262
|
+
};
|
|
235
263
|
}
|
|
236
264
|
|
|
237
265
|
// ─── Voting Cohesion (Rice Index) ────────────────────────────────────────────
|
|
@@ -252,17 +280,22 @@ export function votingCohesion(votes: VotingRecord[]): CohesionResult {
|
|
|
252
280
|
const partyCohesion = [...partyMap.entries()].map(([partyId, counts]) => {
|
|
253
281
|
const total = counts.yes + counts.no;
|
|
254
282
|
const cohesion = total > 0 ? Math.abs((counts.yes - counts.no) / total) : 0;
|
|
255
|
-
return {
|
|
283
|
+
return {
|
|
284
|
+
partyId,
|
|
285
|
+
cohesion,
|
|
286
|
+
yesCount: counts.yes,
|
|
287
|
+
noCount: counts.no,
|
|
288
|
+
size: counts.yes + counts.no + counts.abstain,
|
|
289
|
+
};
|
|
256
290
|
});
|
|
257
291
|
|
|
258
|
-
const totalYes = votes.filter(v => v.vote === 'yes').length;
|
|
259
|
-
const totalNo
|
|
292
|
+
const totalYes = votes.filter((v) => v.vote === 'yes').length;
|
|
293
|
+
const totalNo = votes.filter((v) => v.vote === 'no').length;
|
|
260
294
|
const totalVoting = totalYes + totalNo;
|
|
261
295
|
const chamberCohesion = totalVoting > 0 ? Math.abs((totalYes - totalNo) / totalVoting) : 0;
|
|
262
296
|
|
|
263
297
|
const majority: CohesionResult['majority'] =
|
|
264
|
-
totalYes > totalNo ? 'yes' :
|
|
265
|
-
totalNo > totalYes ? 'no' : 'tie';
|
|
298
|
+
totalYes > totalNo ? 'yes' : totalNo > totalYes ? 'no' : 'tie';
|
|
266
299
|
|
|
267
300
|
return { chamberCohesion, partyCohesion, majority };
|
|
268
301
|
}
|
|
@@ -277,42 +310,56 @@ export function polsbyPopper(areaSqKm: number, perimeterKm: number): PolsbyPoppe
|
|
|
277
310
|
if (areaSqKm <= 0) throw new Error('Area must be positive');
|
|
278
311
|
if (perimeterKm <= 0) throw new Error('Perimeter must be positive');
|
|
279
312
|
|
|
280
|
-
const compactnessScore = (4 * Math.PI * areaSqKm) /
|
|
313
|
+
const compactnessScore = (4 * Math.PI * areaSqKm) / perimeterKm ** 2;
|
|
281
314
|
|
|
282
315
|
const classification: PolsbyPopperResult['classification'] =
|
|
283
|
-
compactnessScore >= 0.
|
|
284
|
-
compactnessScore >= 0.20 ? 'moderate' : 'gerrymandered';
|
|
316
|
+
compactnessScore >= 0.5 ? 'compact' : compactnessScore >= 0.2 ? 'moderate' : 'gerrymandered';
|
|
285
317
|
|
|
286
318
|
return { areaSqKm, perimeterKm, compactnessScore, classification };
|
|
287
319
|
}
|
|
288
320
|
|
|
289
321
|
// ─── Budget Variance ──────────────────────────────────────────────────────────
|
|
290
322
|
|
|
291
|
-
export function budgetVariance(
|
|
292
|
-
lines: BudgetLine[],
|
|
293
|
-
flagThreshold = 0.10,
|
|
294
|
-
): BudgetVarianceResult {
|
|
323
|
+
export function budgetVariance(lines: BudgetLine[], flagThreshold = 0.1): BudgetVarianceResult {
|
|
295
324
|
if (lines.length === 0) throw new Error('No budget lines');
|
|
296
325
|
|
|
297
|
-
const analyzed = lines.map(l => {
|
|
326
|
+
const analyzed = lines.map((l) => {
|
|
298
327
|
const variance = l.actual - l.budgeted;
|
|
299
328
|
const variancePct = l.budgeted !== 0 ? variance / l.budgeted : 0;
|
|
300
329
|
const status: 'favorable' | 'unfavorable' | 'on-target' =
|
|
301
|
-
Math.abs(variancePct) < 0.02 ? 'on-target' :
|
|
302
|
-
|
|
303
|
-
|
|
330
|
+
Math.abs(variancePct) < 0.02 ? 'on-target' : variance < 0 ? 'favorable' : 'unfavorable'; // under-budget = favorable
|
|
331
|
+
return {
|
|
332
|
+
category: l.category,
|
|
333
|
+
budgeted: l.budgeted,
|
|
334
|
+
actual: l.actual,
|
|
335
|
+
variance,
|
|
336
|
+
variancePct,
|
|
337
|
+
status,
|
|
338
|
+
};
|
|
304
339
|
});
|
|
305
340
|
|
|
306
341
|
const totalBudgeted = analyzed.reduce((s, l) => s + l.budgeted, 0);
|
|
307
|
-
const totalActual
|
|
342
|
+
const totalActual = analyzed.reduce((s, l) => s + l.actual, 0);
|
|
308
343
|
const totalVariancePct = totalBudgeted !== 0 ? (totalActual - totalBudgeted) / totalBudgeted : 0;
|
|
309
344
|
const overallStatus: BudgetVarianceResult['overallStatus'] =
|
|
310
|
-
Math.abs(totalVariancePct) < 0.02
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
345
|
+
Math.abs(totalVariancePct) < 0.02
|
|
346
|
+
? 'on-target'
|
|
347
|
+
: totalVariancePct < 0
|
|
348
|
+
? 'favorable'
|
|
349
|
+
: 'unfavorable';
|
|
350
|
+
|
|
351
|
+
const flaggedLines = analyzed
|
|
352
|
+
.filter((l) => Math.abs(l.variancePct) > flagThreshold)
|
|
353
|
+
.map((l) => l.category);
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
lines: analyzed,
|
|
357
|
+
totalBudgeted,
|
|
358
|
+
totalActual,
|
|
359
|
+
totalVariancePct,
|
|
360
|
+
overallStatus,
|
|
361
|
+
flaggedLines,
|
|
362
|
+
};
|
|
316
363
|
}
|
|
317
364
|
|
|
318
365
|
// ─── Receipt ──────────────────────────────────────────────────────────────────
|
|
@@ -329,21 +376,37 @@ export interface GovtAnalysisResult {
|
|
|
329
376
|
|
|
330
377
|
export function buildGovtReceipt(
|
|
331
378
|
result: GovtAnalysisResult,
|
|
332
|
-
options?: GovtReceiptOptions
|
|
379
|
+
options?: GovtReceiptOptions
|
|
333
380
|
): DomainSimulationReceipt {
|
|
334
381
|
const violations: Array<{ criterion: string; message: string }> = [];
|
|
335
382
|
|
|
336
383
|
if (result.permit && !result.permit.approved) {
|
|
337
|
-
violations.push({
|
|
384
|
+
violations.push({
|
|
385
|
+
criterion: 'permit_denied',
|
|
386
|
+
message: `Permit score ${result.permit.compositeScore.toFixed(1)} < ${result.permit.threshold} threshold — application denied`,
|
|
387
|
+
});
|
|
338
388
|
}
|
|
339
389
|
if (result.quorum && !result.quorum.quorumMet) {
|
|
340
|
-
violations.push({
|
|
390
|
+
violations.push({
|
|
391
|
+
criterion: 'no_quorum',
|
|
392
|
+
message: `Only ${result.quorum.presentMembers}/${result.quorum.totalMembers} members present — quorum not met`,
|
|
393
|
+
});
|
|
341
394
|
}
|
|
342
395
|
if (result.compactness && result.compactness.classification === 'gerrymandered') {
|
|
343
|
-
violations.push({
|
|
396
|
+
violations.push({
|
|
397
|
+
criterion: 'gerrymandering',
|
|
398
|
+
message: `District compactness ${result.compactness.compactnessScore.toFixed(3)} < 0.20 — potential gerrymandering`,
|
|
399
|
+
});
|
|
344
400
|
}
|
|
345
|
-
if (
|
|
346
|
-
|
|
401
|
+
if (
|
|
402
|
+
result.budgetVar &&
|
|
403
|
+
result.budgetVar.overallStatus === 'unfavorable' &&
|
|
404
|
+
Math.abs(result.budgetVar.totalVariancePct) > 0.05
|
|
405
|
+
) {
|
|
406
|
+
violations.push({
|
|
407
|
+
criterion: 'budget_overrun',
|
|
408
|
+
message: `Budget overrun ${(result.budgetVar.totalVariancePct * 100).toFixed(1)}% exceeds 5% threshold`,
|
|
409
|
+
});
|
|
347
410
|
}
|
|
348
411
|
|
|
349
412
|
return buildDomainSimulationReceipt({
|
|
@@ -359,7 +422,11 @@ export function buildGovtReceipt(
|
|
|
359
422
|
districtCompactness: result.compactness?.compactnessScore ?? null,
|
|
360
423
|
budgetVariancePct: result.budgetVar?.totalVariancePct ?? null,
|
|
361
424
|
},
|
|
362
|
-
cael: {
|
|
425
|
+
cael: {
|
|
426
|
+
version: 'cael.v1',
|
|
427
|
+
event: 'government_civic.civic_analysis',
|
|
428
|
+
solverType: 'government-civic.permit-scoring',
|
|
429
|
+
},
|
|
363
430
|
acceptance: { accepted: violations.length === 0, violations },
|
|
364
431
|
});
|
|
365
432
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,44 @@
|
|
|
1
1
|
export * from './civicsolver';
|
|
2
|
-
export {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
export {
|
|
3
|
+
createPermitHandler,
|
|
4
|
+
type PermitConfig,
|
|
5
|
+
type PermitState,
|
|
6
|
+
type PermitType,
|
|
7
|
+
type PermitStatus,
|
|
8
|
+
} from './traits/PermitTrait';
|
|
9
|
+
export {
|
|
10
|
+
createPublicMeetingHandler,
|
|
11
|
+
type PublicMeetingConfig,
|
|
12
|
+
type PublicMeetingState,
|
|
13
|
+
type MeetingType,
|
|
14
|
+
type MeetingStatus,
|
|
15
|
+
type AgendaItem,
|
|
16
|
+
} from './traits/PublicMeetingTrait';
|
|
17
|
+
export {
|
|
18
|
+
createServiceRequestHandler,
|
|
19
|
+
type ServiceRequestConfig,
|
|
20
|
+
type ServiceRequestState,
|
|
21
|
+
type ServiceCategory,
|
|
22
|
+
type RequestStatus,
|
|
23
|
+
type PriorityLevel,
|
|
24
|
+
} from './traits/ServiceRequestTrait';
|
|
25
|
+
export {
|
|
26
|
+
createVotingRecordHandler,
|
|
27
|
+
type VotingRecordConfig,
|
|
28
|
+
type VotingRecordState,
|
|
29
|
+
type VoteType,
|
|
30
|
+
type VoteOutcome,
|
|
31
|
+
type VoteCast,
|
|
32
|
+
} from './traits/VotingRecordTrait';
|
|
33
|
+
export {
|
|
34
|
+
createCivicComplianceHandler,
|
|
35
|
+
type CivicComplianceConfig,
|
|
36
|
+
type CivicComplianceState,
|
|
37
|
+
type ComplianceFramework,
|
|
38
|
+
type ComplianceStatus,
|
|
39
|
+
type ComplianceCheck,
|
|
40
|
+
type FoiaRequest,
|
|
41
|
+
} from './traits/CivicComplianceTrait';
|
|
7
42
|
export * from './traits/types';
|
|
8
43
|
|
|
9
44
|
import { createPermitHandler } from './traits/PermitTrait';
|
|
@@ -15,7 +50,14 @@ import { createCivicComplianceHandler } from './traits/CivicComplianceTrait';
|
|
|
15
50
|
export const pluginMeta = {
|
|
16
51
|
name: '@holoscript/plugin-government-civic',
|
|
17
52
|
version: '1.0.0',
|
|
18
|
-
traits: [
|
|
53
|
+
traits: [
|
|
54
|
+
'permit',
|
|
55
|
+
'public_meeting',
|
|
56
|
+
'service_request',
|
|
57
|
+
'voting_record',
|
|
58
|
+
'civic_compliance',
|
|
59
|
+
'civic_decision',
|
|
60
|
+
],
|
|
19
61
|
};
|
|
20
62
|
|
|
21
63
|
// Runtime integration — behavioral trait handler + registrar that wire the
|
package/src/runtime.ts
CHANGED
|
@@ -60,12 +60,16 @@ export interface TraitDispatchContext {
|
|
|
60
60
|
|
|
61
61
|
export interface RuntimeTraitHandler {
|
|
62
62
|
name: string;
|
|
63
|
-
onAttach?: (
|
|
63
|
+
onAttach?: (
|
|
64
|
+
node: unknown,
|
|
65
|
+
config: CivicDecisionTraitConfig,
|
|
66
|
+
context: TraitDispatchContext
|
|
67
|
+
) => void;
|
|
64
68
|
onUpdate?: (
|
|
65
69
|
node: unknown,
|
|
66
70
|
config: CivicDecisionTraitConfig,
|
|
67
71
|
context: TraitDispatchContext,
|
|
68
|
-
delta: number
|
|
72
|
+
delta: number
|
|
69
73
|
) => void;
|
|
70
74
|
}
|
|
71
75
|
|
|
@@ -80,7 +84,7 @@ interface CivicDecisionNode {
|
|
|
80
84
|
function solveOntoNode(
|
|
81
85
|
node: unknown,
|
|
82
86
|
config: CivicDecisionTraitConfig | undefined,
|
|
83
|
-
context: TraitDispatchContext
|
|
87
|
+
context: TraitDispatchContext
|
|
84
88
|
): void {
|
|
85
89
|
const carrier = node as CivicDecisionNode;
|
|
86
90
|
const nodeId = carrier.id ?? carrier.name ?? 'unknown';
|
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
|
|
3
3
|
|
|
4
4
|
export type ComplianceFramework = 'ADA' | 'FOIA' | 'WCAG' | 'OPRA' | 'GDPR' | 'CCPA' | 'section508';
|
|
5
|
-
export type ComplianceStatus =
|
|
5
|
+
export type ComplianceStatus =
|
|
6
|
+
| 'compliant'
|
|
7
|
+
| 'non_compliant'
|
|
8
|
+
| 'pending_review'
|
|
9
|
+
| 'exempt'
|
|
10
|
+
| 'remediation_in_progress';
|
|
6
11
|
|
|
7
12
|
export interface ComplianceCheck {
|
|
8
13
|
id: string;
|
|
@@ -56,10 +61,11 @@ const defaultConfig: CivicComplianceConfig = {
|
|
|
56
61
|
};
|
|
57
62
|
|
|
58
63
|
function computeOverallStatus(checks: ComplianceCheck[]): ComplianceStatus {
|
|
59
|
-
if (checks.some(c => c.status === 'non_compliant')) return 'non_compliant';
|
|
60
|
-
if (checks.some(c => c.status === 'remediation_in_progress')) return 'remediation_in_progress';
|
|
61
|
-
if (checks.some(c => c.status === 'pending_review')) return 'pending_review';
|
|
62
|
-
if (checks.length > 0 && checks.every(c => c.status === 'compliant' || c.status === 'exempt'))
|
|
64
|
+
if (checks.some((c) => c.status === 'non_compliant')) return 'non_compliant';
|
|
65
|
+
if (checks.some((c) => c.status === 'remediation_in_progress')) return 'remediation_in_progress';
|
|
66
|
+
if (checks.some((c) => c.status === 'pending_review')) return 'pending_review';
|
|
67
|
+
if (checks.length > 0 && checks.every((c) => c.status === 'compliant' || c.status === 'exempt'))
|
|
68
|
+
return 'compliant';
|
|
63
69
|
return 'pending_review';
|
|
64
70
|
}
|
|
65
71
|
|
|
@@ -69,7 +75,7 @@ export function createCivicComplianceHandler(): TraitHandler<CivicComplianceConf
|
|
|
69
75
|
defaultConfig,
|
|
70
76
|
onAttach(node: HSPlusNode, config: CivicComplianceConfig, ctx: TraitContext) {
|
|
71
77
|
// Seed initial checks based on declared frameworks
|
|
72
|
-
const checks: ComplianceCheck[] = config.frameworks.map(fw => ({
|
|
78
|
+
const checks: ComplianceCheck[] = config.frameworks.map((fw) => ({
|
|
73
79
|
id: `${config.entityId}-${fw.toLowerCase()}`,
|
|
74
80
|
framework: fw,
|
|
75
81
|
requirement: `${fw} baseline compliance`,
|
|
@@ -86,7 +92,10 @@ export function createCivicComplianceHandler(): TraitHandler<CivicComplianceConf
|
|
|
86
92
|
overdueFoiaCount: 0,
|
|
87
93
|
nonCompliantCount: 0,
|
|
88
94
|
} satisfies CivicComplianceState;
|
|
89
|
-
ctx.emit?.('compliance:initialized', {
|
|
95
|
+
ctx.emit?.('compliance:initialized', {
|
|
96
|
+
entityId: config.entityId,
|
|
97
|
+
frameworks: config.frameworks,
|
|
98
|
+
});
|
|
90
99
|
},
|
|
91
100
|
onDetach(node: HSPlusNode, _config: CivicComplianceConfig, ctx: TraitContext) {
|
|
92
101
|
delete node.__complianceState;
|
|
@@ -109,8 +118,10 @@ export function createCivicComplianceHandler(): TraitHandler<CivicComplianceConf
|
|
|
109
118
|
if (overdue > prevOverdue) {
|
|
110
119
|
ctx.emit?.('compliance:foia_overdue', { entityId: config.entityId, overdueCount: overdue });
|
|
111
120
|
}
|
|
112
|
-
s.openFoiaCount = s.foiaRequests.filter(
|
|
113
|
-
|
|
121
|
+
s.openFoiaCount = s.foiaRequests.filter(
|
|
122
|
+
(r) => r.status === 'received' || r.status === 'processing'
|
|
123
|
+
).length;
|
|
124
|
+
s.nonCompliantCount = s.checks.filter((c) => c.status === 'non_compliant').length;
|
|
114
125
|
s.overallStatus = computeOverallStatus(s.checks);
|
|
115
126
|
},
|
|
116
127
|
onEvent(node: HSPlusNode, config: CivicComplianceConfig, ctx: TraitContext, event: TraitEvent) {
|
|
@@ -118,8 +129,12 @@ export function createCivicComplianceHandler(): TraitHandler<CivicComplianceConf
|
|
|
118
129
|
if (!s) return;
|
|
119
130
|
switch (event.type) {
|
|
120
131
|
case 'compliance:update_check': {
|
|
121
|
-
const { checkId, status, remediation } = event.payload as {
|
|
122
|
-
|
|
132
|
+
const { checkId, status, remediation } = event.payload as {
|
|
133
|
+
checkId: string;
|
|
134
|
+
status: ComplianceStatus;
|
|
135
|
+
remediation?: string;
|
|
136
|
+
};
|
|
137
|
+
const check = s.checks.find((c) => c.id === checkId);
|
|
123
138
|
if (check) {
|
|
124
139
|
check.status = status;
|
|
125
140
|
check.lastChecked = new Date().toISOString();
|
|
@@ -135,28 +150,51 @@ export function createCivicComplianceHandler(): TraitHandler<CivicComplianceConf
|
|
|
135
150
|
if (!req?.requestId) return;
|
|
136
151
|
const receivedAt = new Date();
|
|
137
152
|
// Approx 20 business days = 28 calendar days
|
|
138
|
-
const dueAt = new Date(
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
153
|
+
const dueAt = new Date(
|
|
154
|
+
receivedAt.getTime() + config.foiaResponseDaysLimit * 1.4 * 86_400_000
|
|
155
|
+
);
|
|
156
|
+
s.foiaRequests.push({
|
|
157
|
+
...req,
|
|
158
|
+
requestedAt: receivedAt.toISOString(),
|
|
159
|
+
responseDueAt: dueAt.toISOString(),
|
|
160
|
+
status: 'received',
|
|
161
|
+
});
|
|
162
|
+
s.openFoiaCount = s.foiaRequests.filter(
|
|
163
|
+
(r) => r.status === 'received' || r.status === 'processing'
|
|
164
|
+
).length;
|
|
165
|
+
ctx.emit?.('compliance:foia_received', {
|
|
166
|
+
requestId: req.requestId,
|
|
167
|
+
dueAt: dueAt.toISOString(),
|
|
168
|
+
});
|
|
142
169
|
break;
|
|
143
170
|
}
|
|
144
171
|
case 'compliance:foia_fulfill': {
|
|
145
172
|
const requestId = event.payload?.requestId as string;
|
|
146
|
-
const req = s.foiaRequests.find(r => r.requestId === requestId);
|
|
173
|
+
const req = s.foiaRequests.find((r) => r.requestId === requestId);
|
|
147
174
|
if (req) {
|
|
148
175
|
req.status = 'fulfilled';
|
|
149
|
-
s.openFoiaCount = s.foiaRequests.filter(
|
|
176
|
+
s.openFoiaCount = s.foiaRequests.filter(
|
|
177
|
+
(r) => r.status === 'received' || r.status === 'processing'
|
|
178
|
+
).length;
|
|
150
179
|
ctx.emit?.('compliance:foia_fulfilled', { requestId });
|
|
151
180
|
}
|
|
152
181
|
break;
|
|
153
182
|
}
|
|
154
183
|
case 'compliance:audit_log': {
|
|
155
|
-
const { action, actor, details } = event.payload as {
|
|
156
|
-
|
|
184
|
+
const { action, actor, details } = event.payload as {
|
|
185
|
+
action: string;
|
|
186
|
+
actor: string;
|
|
187
|
+
details: string;
|
|
188
|
+
};
|
|
189
|
+
s.auditLog.push({
|
|
190
|
+
timestamp: new Date().toISOString(),
|
|
191
|
+
action,
|
|
192
|
+
actor,
|
|
193
|
+
details: details ?? '',
|
|
194
|
+
});
|
|
157
195
|
// Prune logs older than retention window
|
|
158
196
|
const cutoff = Date.now() - config.auditLogRetentionDays * 86_400_000;
|
|
159
|
-
s.auditLog = s.auditLog.filter(e => new Date(e.timestamp).getTime() > cutoff);
|
|
197
|
+
s.auditLog = s.auditLog.filter((e) => new Date(e.timestamp).getTime() > cutoff);
|
|
160
198
|
break;
|
|
161
199
|
}
|
|
162
200
|
}
|
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
/** @permit Trait — Building and business permit lifecycle management. @trait permit */
|
|
2
2
|
import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
|
|
3
3
|
|
|
4
|
-
export type PermitType =
|
|
5
|
-
|
|
4
|
+
export type PermitType =
|
|
5
|
+
| 'building'
|
|
6
|
+
| 'business'
|
|
7
|
+
| 'event'
|
|
8
|
+
| 'demolition'
|
|
9
|
+
| 'electrical'
|
|
10
|
+
| 'plumbing'
|
|
11
|
+
| 'sign'
|
|
12
|
+
| 'zoning';
|
|
13
|
+
export type PermitStatus =
|
|
14
|
+
| 'submitted'
|
|
15
|
+
| 'under_review'
|
|
16
|
+
| 'approved'
|
|
17
|
+
| 'denied'
|
|
18
|
+
| 'expired'
|
|
19
|
+
| 'revoked'
|
|
20
|
+
| 'withdrawn';
|
|
6
21
|
|
|
7
22
|
export interface PermitConfig {
|
|
8
23
|
permitType: PermitType;
|
|
@@ -10,7 +25,7 @@ export interface PermitConfig {
|
|
|
10
25
|
applicantName: string;
|
|
11
26
|
projectAddress: string;
|
|
12
27
|
submittedAt: string; // ISO date
|
|
13
|
-
expiresAt?: string;
|
|
28
|
+
expiresAt?: string; // ISO date — when approved permit expires
|
|
14
29
|
reviewDeadlineDays: number;
|
|
15
30
|
showStatusBadge: boolean;
|
|
16
31
|
}
|
|
@@ -74,7 +89,10 @@ export function createPermitHandler(): TraitHandler<PermitConfig> {
|
|
|
74
89
|
const prev = node.__permitState as PermitState | undefined;
|
|
75
90
|
const next = computeState(config, prev);
|
|
76
91
|
if (next.isOverdue && !prev?.isOverdue) {
|
|
77
|
-
ctx.emit?.('permit:overdue', {
|
|
92
|
+
ctx.emit?.('permit:overdue', {
|
|
93
|
+
applicationNumber: config.applicationNumber,
|
|
94
|
+
daysInReview: next.daysInReview,
|
|
95
|
+
});
|
|
78
96
|
}
|
|
79
97
|
if (next.isExpiringSoon && !prev?.isExpiringSoon) {
|
|
80
98
|
ctx.emit?.('permit:expiring_soon', { applicationNumber: config.applicationNumber });
|
|
@@ -93,13 +111,20 @@ export function createPermitHandler(): TraitHandler<PermitConfig> {
|
|
|
93
111
|
const prev = s.status;
|
|
94
112
|
s.status = newStatus;
|
|
95
113
|
s.lastUpdated = new Date().toISOString();
|
|
96
|
-
ctx.emit?.('permit:status_changed', {
|
|
114
|
+
ctx.emit?.('permit:status_changed', {
|
|
115
|
+
from: prev,
|
|
116
|
+
to: newStatus,
|
|
117
|
+
applicationNumber: config.applicationNumber,
|
|
118
|
+
});
|
|
97
119
|
}
|
|
98
120
|
if (event.type === 'permit:add_note') {
|
|
99
121
|
const note = event.payload?.note as string;
|
|
100
122
|
if (note) {
|
|
101
123
|
s.reviewNotes.push(note);
|
|
102
|
-
ctx.emit?.('permit:note_added', {
|
|
124
|
+
ctx.emit?.('permit:note_added', {
|
|
125
|
+
applicationNumber: config.applicationNumber,
|
|
126
|
+
noteCount: s.reviewNotes.length,
|
|
127
|
+
});
|
|
103
128
|
}
|
|
104
129
|
}
|
|
105
130
|
},
|
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
/** @public_meeting Trait — City council and public hearing management. @trait public_meeting */
|
|
2
2
|
import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
|
|
3
3
|
|
|
4
|
-
export type MeetingType =
|
|
5
|
-
|
|
4
|
+
export type MeetingType =
|
|
5
|
+
| 'city_council'
|
|
6
|
+
| 'planning_commission'
|
|
7
|
+
| 'public_hearing'
|
|
8
|
+
| 'zoning_board'
|
|
9
|
+
| 'budget_session'
|
|
10
|
+
| 'town_hall';
|
|
11
|
+
export type MeetingStatus =
|
|
12
|
+
| 'scheduled'
|
|
13
|
+
| 'in_session'
|
|
14
|
+
| 'recess'
|
|
15
|
+
| 'public_comment'
|
|
16
|
+
| 'voting'
|
|
17
|
+
| 'adjourned'
|
|
18
|
+
| 'cancelled';
|
|
6
19
|
|
|
7
20
|
export interface AgendaItem {
|
|
8
21
|
id: string;
|
|
@@ -80,7 +93,9 @@ export function createPublicMeetingHandler(): TraitHandler<PublicMeetingConfig>
|
|
|
80
93
|
const quorumMet = config.membersPresent >= config.quorumRequired;
|
|
81
94
|
if (quorumMet !== s.quorumMet) {
|
|
82
95
|
s.quorumMet = quorumMet;
|
|
83
|
-
ctx.emit?.(quorumMet ? 'meeting:quorum_met' : 'meeting:quorum_lost', {
|
|
96
|
+
ctx.emit?.(quorumMet ? 'meeting:quorum_met' : 'meeting:quorum_lost', {
|
|
97
|
+
meetingId: config.meetingId,
|
|
98
|
+
});
|
|
84
99
|
}
|
|
85
100
|
s.lastUpdated = new Date().toISOString();
|
|
86
101
|
},
|
|
@@ -91,11 +106,16 @@ export function createPublicMeetingHandler(): TraitHandler<PublicMeetingConfig>
|
|
|
91
106
|
case 'meeting:call_to_order':
|
|
92
107
|
s.status = 'in_session';
|
|
93
108
|
s.recordingActive = true;
|
|
94
|
-
ctx.emit?.('meeting:called_to_order', {
|
|
109
|
+
ctx.emit?.('meeting:called_to_order', {
|
|
110
|
+
meetingId: config.meetingId,
|
|
111
|
+
quorumMet: s.quorumMet,
|
|
112
|
+
});
|
|
95
113
|
break;
|
|
96
114
|
case 'meeting:open_public_comment':
|
|
97
115
|
s.status = 'public_comment';
|
|
98
|
-
ctx.emit?.('meeting:public_comment_open', {
|
|
116
|
+
ctx.emit?.('meeting:public_comment_open', {
|
|
117
|
+
speakersRegistered: s.publicSpeakersRegistered,
|
|
118
|
+
});
|
|
99
119
|
break;
|
|
100
120
|
case 'meeting:register_speaker':
|
|
101
121
|
s.publicSpeakersRegistered++;
|
|
@@ -103,19 +123,28 @@ export function createPublicMeetingHandler(): TraitHandler<PublicMeetingConfig>
|
|
|
103
123
|
break;
|
|
104
124
|
case 'meeting:next_speaker':
|
|
105
125
|
s.publicSpeakersHeard++;
|
|
106
|
-
ctx.emit?.('meeting:speaker_started', {
|
|
126
|
+
ctx.emit?.('meeting:speaker_started', {
|
|
127
|
+
heard: s.publicSpeakersHeard,
|
|
128
|
+
remaining: s.publicSpeakersRegistered - s.publicSpeakersHeard,
|
|
129
|
+
});
|
|
107
130
|
break;
|
|
108
131
|
case 'meeting:next_agenda_item':
|
|
109
132
|
if (s.currentAgendaItemIndex < config.agenda.length - 1) {
|
|
110
133
|
s.currentAgendaItemIndex++;
|
|
111
134
|
const item = config.agenda[s.currentAgendaItemIndex];
|
|
112
|
-
ctx.emit?.('meeting:agenda_advanced', {
|
|
135
|
+
ctx.emit?.('meeting:agenda_advanced', {
|
|
136
|
+
index: s.currentAgendaItemIndex,
|
|
137
|
+
item: item?.title,
|
|
138
|
+
});
|
|
113
139
|
}
|
|
114
140
|
break;
|
|
115
141
|
case 'meeting:adjourn':
|
|
116
142
|
s.status = 'adjourned';
|
|
117
143
|
s.recordingActive = false;
|
|
118
|
-
ctx.emit?.('meeting:adjourned', {
|
|
144
|
+
ctx.emit?.('meeting:adjourned', {
|
|
145
|
+
meetingId: config.meetingId,
|
|
146
|
+
minutesElapsed: Math.round(s.minutesElapsed),
|
|
147
|
+
});
|
|
119
148
|
break;
|
|
120
149
|
}
|
|
121
150
|
},
|
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
/** @service_request Trait — 311-style civic service request tracking. @trait service_request */
|
|
2
2
|
import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
|
|
3
3
|
|
|
4
|
-
export type ServiceCategory =
|
|
5
|
-
|
|
4
|
+
export type ServiceCategory =
|
|
5
|
+
| 'pothole'
|
|
6
|
+
| 'streetlight'
|
|
7
|
+
| 'graffiti'
|
|
8
|
+
| 'abandoned_vehicle'
|
|
9
|
+
| 'code_violation'
|
|
10
|
+
| 'tree_hazard'
|
|
11
|
+
| 'water_main'
|
|
12
|
+
| 'sidewalk'
|
|
13
|
+
| 'noise_complaint'
|
|
14
|
+
| 'other';
|
|
15
|
+
export type RequestStatus =
|
|
16
|
+
| 'submitted'
|
|
17
|
+
| 'acknowledged'
|
|
18
|
+
| 'assigned'
|
|
19
|
+
| 'in_progress'
|
|
20
|
+
| 'resolved'
|
|
21
|
+
| 'closed'
|
|
22
|
+
| 'duplicate';
|
|
6
23
|
export type PriorityLevel = 'low' | 'medium' | 'high' | 'urgent';
|
|
7
24
|
|
|
8
25
|
export interface ServiceRequestConfig {
|
|
@@ -11,7 +28,7 @@ export interface ServiceRequestConfig {
|
|
|
11
28
|
description: string;
|
|
12
29
|
location: string;
|
|
13
30
|
coordinates?: { lat: number; lng: number };
|
|
14
|
-
submittedAt: string;
|
|
31
|
+
submittedAt: string; // ISO datetime
|
|
15
32
|
priority: PriorityLevel;
|
|
16
33
|
targetResolutionDays: number; // SLA days by priority
|
|
17
34
|
department: string;
|
|
@@ -100,7 +117,11 @@ export function createServiceRequestHandler(): TraitHandler<ServiceRequestConfig
|
|
|
100
117
|
const prev = s.status;
|
|
101
118
|
s.status = newStatus;
|
|
102
119
|
s.lastStatusChange = new Date().toISOString();
|
|
103
|
-
ctx.emit?.('sr:status_changed', {
|
|
120
|
+
ctx.emit?.('sr:status_changed', {
|
|
121
|
+
from: prev,
|
|
122
|
+
to: newStatus,
|
|
123
|
+
requestId: config.requestId,
|
|
124
|
+
});
|
|
104
125
|
break;
|
|
105
126
|
}
|
|
106
127
|
case 'sr:add_update': {
|
|
@@ -108,7 +129,10 @@ export function createServiceRequestHandler(): TraitHandler<ServiceRequestConfig
|
|
|
108
129
|
const author = (event.payload?.author as string) ?? 'staff';
|
|
109
130
|
if (message) {
|
|
110
131
|
s.updates.push({ timestamp: new Date().toISOString(), message, author });
|
|
111
|
-
ctx.emit?.('sr:update_added', {
|
|
132
|
+
ctx.emit?.('sr:update_added', {
|
|
133
|
+
requestId: config.requestId,
|
|
134
|
+
updateCount: s.updates.length,
|
|
135
|
+
});
|
|
112
136
|
}
|
|
113
137
|
break;
|
|
114
138
|
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
/** @voting_record Trait — Public voting record and election results display. @trait voting_record */
|
|
2
2
|
import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
|
|
3
3
|
|
|
4
|
-
export type VoteType =
|
|
4
|
+
export type VoteType =
|
|
5
|
+
| 'council_vote'
|
|
6
|
+
| 'ballot_measure'
|
|
7
|
+
| 'election'
|
|
8
|
+
| 'referendum'
|
|
9
|
+
| 'committee_vote';
|
|
5
10
|
export type VoteOutcome = 'passed' | 'failed' | 'tabled' | 'withdrawn' | 'tied' | 'pending';
|
|
6
11
|
|
|
7
12
|
export interface VoteCast {
|
|
@@ -71,7 +76,9 @@ export function createVotingRecordHandler(): TraitHandler<VotingRecordConfig> {
|
|
|
71
76
|
node.__votingState = {
|
|
72
77
|
outcome: 'pending' as VoteOutcome,
|
|
73
78
|
votes: [],
|
|
74
|
-
ayeCount: 0,
|
|
79
|
+
ayeCount: 0,
|
|
80
|
+
nayCount: 0,
|
|
81
|
+
abstainCount: 0,
|
|
75
82
|
absentCount: config.eligibleVoters.length,
|
|
76
83
|
totalEligible: config.eligibleVoters.length,
|
|
77
84
|
participationRate: 0,
|
|
@@ -98,24 +105,35 @@ export function createVotingRecordHandler(): TraitHandler<VotingRecordConfig> {
|
|
|
98
105
|
const cast = event.payload as unknown as VoteCast;
|
|
99
106
|
if (!cast?.memberId || !cast?.vote) return;
|
|
100
107
|
// Replace if already voted
|
|
101
|
-
const existing = s.votes.findIndex(v => v.memberId === cast.memberId);
|
|
108
|
+
const existing = s.votes.findIndex((v) => v.memberId === cast.memberId);
|
|
102
109
|
if (existing >= 0) s.votes.splice(existing, 1);
|
|
103
110
|
s.votes.push({ ...cast, timestamp: new Date().toISOString() });
|
|
104
111
|
// Recount
|
|
105
|
-
s.ayeCount = s.votes.filter(v => v.vote === 'aye').length;
|
|
106
|
-
s.nayCount = s.votes.filter(v => v.vote === 'nay').length;
|
|
107
|
-
s.abstainCount = s.votes.filter(v => v.vote === 'abstain').length;
|
|
112
|
+
s.ayeCount = s.votes.filter((v) => v.vote === 'aye').length;
|
|
113
|
+
s.nayCount = s.votes.filter((v) => v.vote === 'nay').length;
|
|
114
|
+
s.abstainCount = s.votes.filter((v) => v.vote === 'abstain').length;
|
|
108
115
|
s.absentCount = s.totalEligible - s.votes.length;
|
|
109
116
|
s.participationRate = s.votes.length / (s.totalEligible || 1);
|
|
110
117
|
s.quorumMet = s.participationRate > 0.5;
|
|
111
|
-
ctx.emit?.('vote:cast', {
|
|
118
|
+
ctx.emit?.('vote:cast', {
|
|
119
|
+
voteId: config.voteId,
|
|
120
|
+
memberId: cast.memberId,
|
|
121
|
+
vote: cast.vote,
|
|
122
|
+
ayeCount: s.ayeCount,
|
|
123
|
+
nayCount: s.nayCount,
|
|
124
|
+
});
|
|
112
125
|
break;
|
|
113
126
|
}
|
|
114
127
|
case 'vote:close':
|
|
115
128
|
s.isOpen = false;
|
|
116
129
|
s.closedAt = new Date().toISOString();
|
|
117
130
|
s.outcome = computeOutcome(s, config);
|
|
118
|
-
ctx.emit?.('vote:closed', {
|
|
131
|
+
ctx.emit?.('vote:closed', {
|
|
132
|
+
voteId: config.voteId,
|
|
133
|
+
outcome: s.outcome,
|
|
134
|
+
ayeCount: s.ayeCount,
|
|
135
|
+
nayCount: s.nayCount,
|
|
136
|
+
});
|
|
119
137
|
break;
|
|
120
138
|
case 'vote:table':
|
|
121
139
|
s.isOpen = false;
|
package/src/traits/types.ts
CHANGED
|
@@ -1,4 +1,25 @@
|
|
|
1
|
-
export interface HSPlusNode {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
export interface HSPlusNode {
|
|
2
|
+
id?: string;
|
|
3
|
+
properties?: Record<string, unknown>;
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
}
|
|
6
|
+
export interface TraitContext {
|
|
7
|
+
emit?: (event: string, payload?: unknown) => void;
|
|
8
|
+
getState?: () => Record<string, unknown>;
|
|
9
|
+
setState?: (updates: Record<string, unknown>) => void;
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
}
|
|
12
|
+
export interface TraitEvent {
|
|
13
|
+
type: string;
|
|
14
|
+
source?: string;
|
|
15
|
+
payload?: Record<string, unknown>;
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
}
|
|
18
|
+
export interface TraitHandler<TConfig = unknown> {
|
|
19
|
+
name: string;
|
|
20
|
+
defaultConfig: TConfig;
|
|
21
|
+
onAttach(node: HSPlusNode, config: TConfig, ctx: TraitContext): void;
|
|
22
|
+
onDetach(node: HSPlusNode, config: TConfig, ctx: TraitContext): void;
|
|
23
|
+
onUpdate(node: HSPlusNode, config: TConfig, ctx: TraitContext, delta: number): void;
|
|
24
|
+
onEvent(node: HSPlusNode, config: TConfig, ctx: TraitContext, event: TraitEvent): void;
|
|
25
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -1 +1,10 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "dist",
|
|
5
|
+
"rootDir": "src",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"declarationMap": true
|
|
8
|
+
},
|
|
9
|
+
"include": ["src"]
|
|
10
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025-2026 HoloScript Contributors
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|