@holoscript/plugin-holonews 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
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.
package/package.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "@holoscript/plugin-holonews",
3
+ "version": "1.0.0",
4
+ "description": "HoloNews verifiable-claim display primitive (@claim_kiosk trait, proof-adjacency guard)",
5
+ "main": "src/index.mjs",
6
+ "type": "module",
7
+ "peerDependencies": {
8
+ "@holoscript/core": ">=8.0.0"
9
+ },
10
+ "license": "MIT",
11
+ "scripts": {
12
+ "test": "vitest run --passWithNoTests",
13
+ "test:coverage": "vitest run --coverage --passWithNoTests"
14
+ }
15
+ }
package/src/index.mjs ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @holoscript/plugin-holonews
3
+ *
4
+ * HoloNews Plugin — verifiable-claim news layer for HoloLand.
5
+ *
6
+ * Phase 0 surface:
7
+ * - @claim_kiosk trait: in-zone proof-display primitive (T-NEWS-KIOSK)
8
+ * - ProofAdjacencyGuard: structural invariant enforcement (G.910.01 / T-NEWS-CI)
9
+ *
10
+ * The verifier is `verify_cael_trace` in packages/mcp-server/src/simulation-tools.ts.
11
+ * The kiosk binds its output (a CaelReceipt) to a spatial in-zone object and
12
+ * enforces the proof-adjacency wall across all compile targets.
13
+ *
14
+ * NOT in this phase:
15
+ * - News plugin + Formalizer + GDELT firehose (Phase 2, B.4 gap #4)
16
+ * - `hololand_capture_runtime_receipt` real-verification wiring (Phase 1, B.4 gap #2)
17
+ * - Durable world content (Phase 1, B.4 gap #3)
18
+ * - Stubbed hololandRoutes wiring (Phase 2, B.4 gap #5)
19
+ */
20
+
21
+ export {
22
+ createClaimKioskHandler,
23
+ buildCaelReceipt,
24
+ defaultClaimKioskConfig,
25
+ } from './traits/claim-kiosk-trait.mjs';
26
+
27
+ export {
28
+ resolveProofAdjacencyPolicy,
29
+ auditReceiptForWallInvariant,
30
+ buildKioskDisplayModel,
31
+ } from './traits/proof-adjacency-guard.mjs';
32
+
33
+ import { createClaimKioskHandler } from './traits/claim-kiosk-trait.mjs';
34
+
35
+ export const pluginMeta = {
36
+ name: '@holoscript/plugin-holonews',
37
+ version: '1.0.0',
38
+ traits: ['claim_kiosk'],
39
+ handlers: [createClaimKioskHandler()],
40
+ description: 'HoloNews verifiable-claim display primitive for HoloLand in-zone objects.',
41
+ };
@@ -0,0 +1,192 @@
1
+ /**
2
+ * ClaimKioskTrait — pure ESM implementation of the @claim_kiosk trait handler.
3
+ *
4
+ * Binds a CAEL receipt to a renderable in-zone HoloLand object and enforces
5
+ * the proof-adjacency wall invariant (G.910.01).
6
+ *
7
+ * This file is pure .mjs so it can be imported by Node-native scripts tests
8
+ * (scripts/__tests__/holonews-proof-adjacency.test.mjs) without a TS loader.
9
+ * The TypeScript wrapper (ClaimKioskTrait.ts) re-exports from this file.
10
+ */
11
+
12
+ import {
13
+ buildKioskDisplayModel,
14
+ auditReceiptForWallInvariant,
15
+ } from './proof-adjacency-guard.mjs';
16
+
17
+ // ── defaultConfig ──────────────────────────────────────────────────────────
18
+
19
+ export const defaultClaimKioskConfig = {
20
+ canRenderWall: true,
21
+ label: 'Verified Claim Kiosk',
22
+ showEnvelope: true,
23
+ loudAuditFailures: true,
24
+ };
25
+
26
+ // ── Handler factory ────────────────────────────────────────────────────────
27
+
28
+ /**
29
+ * Create the @claim_kiosk trait handler.
30
+ *
31
+ * @returns {{ name: string, defaultConfig: object, onAttach: Function,
32
+ * onDetach: Function, onUpdate: Function, onEvent: Function }}
33
+ */
34
+ export function createClaimKioskHandler() {
35
+ return {
36
+ name: 'claim_kiosk',
37
+ defaultConfig: defaultClaimKioskConfig,
38
+
39
+ onAttach(node, _config, ctx) {
40
+ node.__kioskState = {
41
+ receipt: null,
42
+ displayModel: null,
43
+ envelopeOverrides: {},
44
+ idle: true,
45
+ };
46
+ ctx.emit?.('kiosk:ready');
47
+ },
48
+
49
+ onDetach(node, _config, ctx) {
50
+ delete node.__kioskState;
51
+ ctx.emit?.('kiosk:offline');
52
+ },
53
+
54
+ onUpdate(_node, _config, _ctx, _deltaMs) {
55
+ // Event-driven — no continuous update logic.
56
+ },
57
+
58
+ onEvent(node, config, ctx, event) {
59
+ const state = node.__kioskState;
60
+ if (!state) return;
61
+
62
+ // ── kiosk:receipt_bind ────────────────────────────────────────────
63
+ if (event.type === 'kiosk:receipt_bind') {
64
+ const receipt = event.payload?.receipt;
65
+ if (!receipt) {
66
+ ctx.emit?.('kiosk:error', { reason: 'receipt_bind received no receipt in payload' });
67
+ return;
68
+ }
69
+
70
+ const violations = auditReceiptForWallInvariant(receipt);
71
+ if (violations.length > 0 && config.loudAuditFailures) {
72
+ ctx.emit?.('kiosk:audit_violation', { violations, receiptId: receipt.receiptId });
73
+ }
74
+
75
+ const displayModel = buildKioskDisplayModel(receipt, config.canRenderWall);
76
+
77
+ state.receipt = receipt;
78
+ state.displayModel = displayModel;
79
+ state.envelopeOverrides = {};
80
+ state.idle = false;
81
+
82
+ ctx.emit?.('kiosk:display_ready', { displayModel });
83
+
84
+ if (displayModel.showBadge) {
85
+ // Badge shown — wall MUST be co-rendered (enforced by policy='badge-with-wall').
86
+ ctx.emit?.('kiosk:badge_render', {
87
+ claimText: displayModel.claimText,
88
+ wallText: displayModel.wallText,
89
+ verifyUrl: displayModel.verifyUrl,
90
+ });
91
+ } else if (displayModel.policy === 'badge-suppressed-rerun-only') {
92
+ ctx.emit?.('kiosk:badge_suppressed', {
93
+ claimText: displayModel.claimText,
94
+ verifyUrl: displayModel.verifyUrl,
95
+ reason:
96
+ receipt.notProvenWall == null || receipt.notProvenWall.trim() === ''
97
+ ? 'missing_wall_text'
98
+ : 'target_cannot_render_wall',
99
+ });
100
+ }
101
+ // policy === 'no-badge': labeled or pending — no badge event.
102
+ return;
103
+ }
104
+
105
+ // ── kiosk:envelope_update ─────────────────────────────────────────
106
+ if (event.type === 'kiosk:envelope_update') {
107
+ if (!state.receipt?.envelope) {
108
+ ctx.emit?.('kiosk:error', {
109
+ reason: 'envelope_update received but no envelope on receipt',
110
+ });
111
+ return;
112
+ }
113
+ const paramName = event.payload?.paramName;
114
+ const value = event.payload?.value;
115
+ if (paramName == null || value == null) {
116
+ ctx.emit?.('kiosk:error', { reason: 'envelope_update missing paramName or value' });
117
+ return;
118
+ }
119
+ const param = state.receipt.envelope.params.find((p) => p.name === paramName);
120
+ if (!param) {
121
+ ctx.emit?.('kiosk:error', {
122
+ reason: `envelope_update: unknown param "${paramName}"`,
123
+ });
124
+ return;
125
+ }
126
+ const clamped = Math.min(Math.max(value, param.min), param.max);
127
+ state.envelopeOverrides[paramName] = clamped;
128
+
129
+ ctx.emit?.('kiosk:envelope_changed', {
130
+ paramName,
131
+ value: clamped,
132
+ allOverrides: { ...state.envelopeOverrides },
133
+ boundaryDescription: state.receipt.envelope.boundaryDescription,
134
+ });
135
+ return;
136
+ }
137
+
138
+ // ── kiosk:rerun_request ───────────────────────────────────────────
139
+ if (event.type === 'kiosk:rerun_request') {
140
+ if (!state.receipt) {
141
+ ctx.emit?.('kiosk:error', { reason: 'rerun_request but no receipt bound' });
142
+ return;
143
+ }
144
+ ctx.emit?.('kiosk:rerun_initiated', {
145
+ verifyUrl: state.receipt.verifyUrl,
146
+ traceId: state.receipt.traceId,
147
+ envelopeOverrides: { ...state.envelopeOverrides },
148
+ });
149
+ return;
150
+ }
151
+
152
+ // ── kiosk:reset ───────────────────────────────────────────────────
153
+ if (event.type === 'kiosk:reset') {
154
+ state.receipt = null;
155
+ state.displayModel = null;
156
+ state.envelopeOverrides = {};
157
+ state.idle = true;
158
+ ctx.emit?.('kiosk:ready');
159
+ }
160
+ },
161
+ };
162
+ }
163
+
164
+ // ── buildCaelReceipt ───────────────────────────────────────────────────────
165
+
166
+ /**
167
+ * Build a CaelReceipt from verify_cael_trace output fields plus the required
168
+ * human-authored claim text and not-proven wall.
169
+ *
170
+ * @param {{
171
+ * receiptId: string, traceId: string, verifyUrl: string,
172
+ * hashChainValid: boolean, replayValid: boolean,
173
+ * claimText: string, notProvenWall: string,
174
+ * sealedAt?: string,
175
+ * envelope?: object,
176
+ * }} opts
177
+ */
178
+ export function buildCaelReceipt(opts) {
179
+ const verdict = opts.hashChainValid && opts.replayValid ? 'proven' : 'labeled';
180
+ return {
181
+ receiptId: opts.receiptId,
182
+ traceId: opts.traceId,
183
+ verifyUrl: opts.verifyUrl,
184
+ verdict,
185
+ hashChainValid: opts.hashChainValid,
186
+ replayValid: opts.replayValid,
187
+ sealedAt: opts.sealedAt ?? new Date().toISOString(),
188
+ claimText: opts.claimText,
189
+ notProvenWall: opts.notProvenWall,
190
+ ...(opts.envelope != null ? { envelope: opts.envelope } : {}),
191
+ };
192
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * ProofAdjacencyGuard — pure ESM implementation of the proof-adjacency wall invariant.
3
+ *
4
+ * INVARIANT (G.910.01):
5
+ * A "proven" badge MAY ONLY be displayed when the not-proven boundary text
6
+ * (`notProvenWall`) is rendered at equal or greater prominence on the SAME
7
+ * surface in the SAME viewport.
8
+ *
9
+ * Any compile target that cannot honour this constraint MUST suppress the
10
+ * badge entirely and show the claim text + re-run link only.
11
+ *
12
+ * This file is pure .mjs so it can be imported by Node-native scripts tests
13
+ * (scripts/__tests__/holonews-proof-adjacency.test.mjs) without a TS loader.
14
+ * The TypeScript wrappers (ProofAdjacencyGuard.ts) re-export from this file.
15
+ *
16
+ * FALSIFICATION CONTRACT (T-NEWS-CI):
17
+ * `scripts/__tests__/holonews-proof-adjacency.test.mjs` checks every branch
18
+ * of `resolveProofAdjacencyPolicy` at build time. A CI failure there means
19
+ * this invariant has been violated.
20
+ */
21
+
22
+ /**
23
+ * Resolve which proof-adjacency policy applies for a given receipt and compile
24
+ * target capability.
25
+ *
26
+ * Pure function — no side effects, no I/O, deterministic output.
27
+ *
28
+ * @param {import('./types.js').CaelReceipt} receipt
29
+ * @param {boolean} canRenderWall Whether the current compile target can co-render
30
+ * the not-proven wall alongside the badge in the same viewport.
31
+ * @returns {import('./types.js').ProofAdjacencyPolicy}
32
+ */
33
+ export function resolveProofAdjacencyPolicy(receipt, canRenderWall) {
34
+ // If the verdict is not proven, there is never a badge to display.
35
+ if (receipt.verdict !== 'proven') {
36
+ return 'no-badge';
37
+ }
38
+
39
+ // A proven verdict with no wall text means the not-proven boundary was not
40
+ // authored. Without it the badge is structurally unanchored — suppress.
41
+ if (!receipt.notProvenWall || receipt.notProvenWall.trim() === '') {
42
+ return 'badge-suppressed-rerun-only';
43
+ }
44
+
45
+ // The target claims it can render the wall. Trust it — but the CI test
46
+ // verifies that this branch is never reached without a non-empty wall.
47
+ if (canRenderWall) {
48
+ return 'badge-with-wall';
49
+ }
50
+
51
+ // Target cannot render the wall (OG card, social crop, etc.) — suppress.
52
+ return 'badge-suppressed-rerun-only';
53
+ }
54
+
55
+ /**
56
+ * Validate that a CaelReceipt meets the minimum requirements for the
57
+ * proof-adjacency invariant.
58
+ *
59
+ * Returns a list of violation strings. Empty array means the receipt is
60
+ * structurally sound.
61
+ *
62
+ * @param {import('./types.js').CaelReceipt} receipt
63
+ * @returns {string[]}
64
+ */
65
+ export function auditReceiptForWallInvariant(receipt) {
66
+ const violations = [];
67
+
68
+ if (receipt.verdict === 'proven') {
69
+ if (!receipt.notProvenWall || receipt.notProvenWall.trim() === '') {
70
+ violations.push(
71
+ `Receipt ${receipt.receiptId}: verdict is "proven" but notProvenWall is absent or empty. ` +
72
+ `The proof-adjacency wall cannot be rendered — badge will be suppressed on all targets.`,
73
+ );
74
+ }
75
+ if (!receipt.verifyUrl || !receipt.verifyUrl.startsWith('http')) {
76
+ violations.push(
77
+ `Receipt ${receipt.receiptId}: verdict is "proven" but verifyUrl is missing or invalid. ` +
78
+ `Readers cannot independently re-run the proof.`,
79
+ );
80
+ }
81
+ if (!receipt.hashChainValid) {
82
+ violations.push(
83
+ `Receipt ${receipt.receiptId}: verdict is "proven" but hashChainValid is false. ` +
84
+ `This is a contradictory state — the receipt is structurally corrupt.`,
85
+ );
86
+ }
87
+ if (!receipt.replayValid) {
88
+ violations.push(
89
+ `Receipt ${receipt.receiptId}: verdict is "proven" but replayValid is false. ` +
90
+ `This is a contradictory state — the receipt is structurally corrupt.`,
91
+ );
92
+ }
93
+ }
94
+
95
+ return violations;
96
+ }
97
+
98
+ /**
99
+ * Build the display model for a kiosk render from a receipt and a target
100
+ * capability flag.
101
+ *
102
+ * This is the single data-transformation step between a raw receipt and the
103
+ * in-zone object's render output. Compile targets consume this record; they
104
+ * do not inspect the raw receipt directly.
105
+ *
106
+ * @param {import('./types.js').CaelReceipt} receipt
107
+ * @param {boolean} canRenderWall
108
+ * @returns {{
109
+ * policy: import('./types.js').ProofAdjacencyPolicy,
110
+ * claimText: string,
111
+ * wallText: string | undefined,
112
+ * showBadge: boolean,
113
+ * verifyUrl: string | undefined,
114
+ * showEnvelope: boolean,
115
+ * auditViolations: string[],
116
+ * }}
117
+ */
118
+ export function buildKioskDisplayModel(receipt, canRenderWall) {
119
+ const policy = resolveProofAdjacencyPolicy(receipt, canRenderWall);
120
+ const auditViolations = auditReceiptForWallInvariant(receipt);
121
+
122
+ return {
123
+ policy,
124
+ claimText: receipt.claimText,
125
+ wallText: policy === 'badge-with-wall' ? receipt.notProvenWall : undefined,
126
+ showBadge: policy === 'badge-with-wall',
127
+ verifyUrl: receipt.verdict === 'proven' ? receipt.verifyUrl : undefined,
128
+ showEnvelope: policy !== 'no-badge' && receipt.envelope != null,
129
+ auditViolations,
130
+ };
131
+ }