@mindrian_os/install 1.13.0-beta.17 → 1.13.0-beta.19
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/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +26 -0
- package/commands/act.md +1 -0
- package/commands/admin.md +1 -0
- package/commands/analyze-needs.md +2 -0
- package/commands/analyze-systems.md +2 -0
- package/commands/analyze-timing.md +2 -0
- package/commands/auto-explore.md +2 -0
- package/commands/beautiful-question.md +2 -0
- package/commands/brain-derive.md +2 -0
- package/commands/build-knowledge.md +2 -0
- package/commands/build-thesis.md +2 -0
- package/commands/causal.md +2 -0
- package/commands/challenge-assumptions.md +2 -0
- package/commands/compare-ventures.md +2 -0
- package/commands/dashboard.md +2 -1
- package/commands/deep-grade.md +2 -0
- package/commands/diagnose.md +21 -1
- package/commands/diagnostics.md +14 -3
- package/commands/doctor.md +4 -1
- package/commands/dogfood-flush.md +92 -0
- package/commands/dominant-designs.md +2 -0
- package/commands/explain-decision.md +2 -0
- package/commands/explore-domains.md +2 -0
- package/commands/explore-futures.md +2 -0
- package/commands/explore-trends.md +2 -0
- package/commands/export.md +1 -0
- package/commands/feynman-timeline-refresh.md +2 -0
- package/commands/file-meeting.md +2 -0
- package/commands/find-analogies.md +1 -0
- package/commands/find-bottlenecks.md +2 -0
- package/commands/find-connections.md +2 -0
- package/commands/funding.md +1 -0
- package/commands/grade.md +2 -0
- package/commands/graph.md +1 -0
- package/commands/hat-briefing.md +1 -0
- package/commands/heal.md +22 -170
- package/commands/help.md +54 -334
- package/commands/hmi-status.md +23 -144
- package/commands/jtbd.md +1 -0
- package/commands/leadership.md +2 -0
- package/commands/lean-canvas.md +2 -0
- package/commands/macro-trends.md +2 -0
- package/commands/map-unknowns.md +2 -0
- package/commands/memory.md +1 -0
- package/commands/models.md +1 -0
- package/commands/mos-reason.md +2 -0
- package/commands/mos.md +139 -0
- package/commands/mullins.md +2 -0
- package/commands/mva-brief.md +2 -0
- package/commands/mva-option.md +2 -0
- package/commands/new-project.md +2 -0
- package/commands/onboard.md +20 -7
- package/commands/operator.md +1 -0
- package/commands/opportunities.md +1 -0
- package/commands/organize.md +22 -469
- package/commands/persona.md +1 -0
- package/commands/pipeline.md +2 -0
- package/commands/present.md +1 -0
- package/commands/publish.md +2 -0
- package/commands/query.md +24 -102
- package/commands/radar.md +2 -0
- package/commands/reanalyze.md +1 -0
- package/commands/research.md +2 -0
- package/commands/room.md +2 -0
- package/commands/rooms.md +1 -0
- package/commands/root-cause.md +2 -0
- package/commands/rs-experts.md +1 -0
- package/commands/rs-explain.md +1 -0
- package/commands/rs-fetch.md +1 -0
- package/commands/rs-thesis.md +1 -0
- package/commands/scenario-plan.md +2 -0
- package/commands/scheduled-tasks.md +1 -0
- package/commands/score-innovation.md +2 -0
- package/commands/scout.md +1 -0
- package/commands/setup.md +2 -0
- package/commands/snapshot.md +2 -0
- package/commands/speakers.md +1 -0
- package/commands/splash.md +5 -2
- package/commands/status.md +1 -0
- package/commands/structure-argument.md +2 -0
- package/commands/suggest-next.md +2 -0
- package/commands/systems-thinking.md +2 -0
- package/commands/think-hats.md +2 -0
- package/commands/update.md +2 -0
- package/commands/user-needs.md +2 -0
- package/commands/validate.md +2 -0
- package/commands/value-proposition.md +2 -0
- package/commands/vault.md +2 -0
- package/commands/visualize.md +24 -29
- package/commands/whitespace.md +2 -1
- package/commands/wiki.md +1 -0
- package/hooks/hooks.json +22 -88
- package/lib/agents/auto-explore-agent.cjs +82 -0
- package/lib/core/breakthrough/canary.cjs +134 -0
- package/lib/core/breakthrough/canary.test.cjs +136 -0
- package/lib/core/breakthrough/detectors.cjs +359 -0
- package/lib/core/breakthrough/detectors.test.cjs +333 -0
- package/lib/core/breakthrough/ethics-fence.cjs +127 -0
- package/lib/core/breakthrough/ethics-fence.test.cjs +178 -0
- package/lib/core/breakthrough/resurfacing.cjs +150 -0
- package/lib/core/breakthrough/resurfacing.test.cjs +233 -0
- package/lib/core/breakthrough/review-queue.cjs +154 -0
- package/lib/core/breakthrough/review-queue.test.cjs +160 -0
- package/lib/core/breakthrough/scanner-d17-d18.test.cjs +229 -0
- package/lib/core/breakthrough/scanner.cjs +426 -0
- package/lib/core/breakthrough/scanner.test.cjs +267 -0
- package/lib/core/breakthrough/schema.cjs +164 -0
- package/lib/core/breakthrough/schema.test.cjs +256 -0
- package/lib/core/breakthrough/scoring.cjs +293 -0
- package/lib/core/breakthrough/scoring.test.cjs +423 -0
- package/lib/core/breakthrough/verb-dispatch.cjs +221 -0
- package/lib/core/breakthrough/verb-dispatch.test.cjs +185 -0
- package/lib/core/breakthrough/voice-scaffold.cjs +247 -0
- package/lib/core/breakthrough/voice-scaffold.test.cjs +251 -0
- package/lib/core/first-touch-version-stamper.cjs +113 -0
- package/lib/core/larry-thinness-acknowledgment.cjs +64 -0
- package/lib/core/larry-thinness-acknowledgment.test.cjs +97 -0
- package/lib/core/llm-name-suggester.cjs +194 -0
- package/lib/core/llm-name-suggester.test.cjs +132 -0
- package/lib/core/mva-orchestrator.cjs +41 -0
- package/lib/core/mva-telemetry.cjs +31 -143
- package/lib/core/navigation/edges.cjs +35 -0
- package/lib/core/navigation/memory-events.cjs +126 -0
- package/lib/core/room-auto-create.cjs +318 -0
- package/lib/core/room-auto-create.test.cjs +198 -0
- package/lib/core/room-discard-cascade.cjs +225 -0
- package/lib/core/room-discard-cascade.test.cjs +135 -0
- package/lib/core/room-name-validator.cjs +132 -0
- package/lib/core/room-name-validator.test.cjs +156 -0
- package/lib/core/room-naming-selector.cjs +357 -0
- package/lib/core/room-naming-selector.test.cjs +277 -0
- package/lib/core/room-receipt-emit.cjs +63 -0
- package/lib/core/room-skeleton-scaffold.cjs +315 -0
- package/lib/core/room-skeleton-scaffold.test.cjs +291 -0
- package/lib/core/stale-copy-scanner.cjs +190 -0
- package/lib/core/state-aware-router.cjs +78 -0
- package/lib/core/telemetry/schema.cjs +168 -0
- package/lib/core/telemetry/schema.test.cjs +124 -0
- package/lib/core/telemetry/validator.cjs +197 -0
- package/lib/core/telemetry/validator.test.cjs +188 -0
- package/lib/core/telemetry/writer.cjs +141 -0
- package/lib/core/telemetry/writer.test.cjs +331 -0
- package/lib/core/terminal-capability.cjs +88 -0
- package/lib/core/venture-shape-nudge.cjs +163 -0
- package/lib/core/venture-shape-nudge.test.cjs +161 -0
- package/lib/core/visual-ops.cjs +70 -2
- package/lib/hmi/selector-dispatcher.cjs +90 -1
- package/lib/hmi/shape-f7-breakthrough-renderer.cjs +222 -0
- package/lib/hmi/shape-f7-breakthrough-renderer.test.cjs +233 -0
- package/lib/memory/body-shape-coverage.test.cjs +268 -0
- package/lib/memory/doctor-deprecation-surface.test.cjs +185 -0
- package/lib/memory/first-touch-version.test.cjs +198 -0
- package/lib/memory/help-coverage.test.cjs +108 -0
- package/lib/memory/help-renderer.test.cjs +145 -0
- package/lib/memory/palette-consistency.test.cjs +127 -0
- package/lib/memory/pending-tension-store.cjs +80 -0
- package/lib/memory/render-v2-disposition.test.cjs +199 -0
- package/lib/memory/run-feynman-tests.cjs +213 -0
- package/lib/memory/sessionstart-coordinator.test.cjs +446 -0
- package/lib/memory/skill-vs-code-drift.test.cjs +257 -0
- package/lib/memory/soft-alias.test.cjs +144 -0
- package/lib/memory/stale-copy-scanner.test.cjs +291 -0
- package/lib/memory/state-aware-router.test.cjs +90 -0
- package/lib/memory/statusline-two-row.test.cjs +338 -0
- package/lib/memory/terminal-capability.test.cjs +155 -0
- package/lib/render/ROOM.md +74 -22
- package/lib/sessionstart/budget-compressor.cjs +130 -0
- package/lib/sessionstart/contributor-interface.cjs +134 -0
- package/lib/sessionstart/contributor-isolator.cjs +128 -0
- package/lib/sessionstart/precedence-ladder.cjs +47 -0
- package/lib/statusline/governing-thought-truncator.cjs +45 -0
- package/lib/statusline/two-row-renderer.cjs +186 -0
- package/lib/statusline/version-resolver.cjs +81 -0
- package/package.json +1 -1
- package/references/visual/ROOM.md +55 -0
- package/references/visual/palette.json +54 -0
- package/skills/larry-personality/SKILL.md +34 -0
- package/skills/ui-system/SKILL.md +109 -1
- package/skills/ui-system/rules/dual-palette.md +156 -0
- package/skills/ui-system/rules/glyph-disambiguation.md +171 -0
- package/skills/ui-system/rules/shape-f-zero-and-six.md +169 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
3
|
+
*
|
|
4
|
+
* Phase 121-00 -- emit-time validator (Canon Part 8 constitutional gate).
|
|
5
|
+
*
|
|
6
|
+
* Every telemetry emit() routes through validateEventPayload() BEFORE the
|
|
7
|
+
* fs.appendFileSync at the writer. This is the single chokepoint that
|
|
8
|
+
* structurally enforces Canon Part 8 (Graph Boundary):
|
|
9
|
+
*
|
|
10
|
+
* LOCAL data -> BRAIN: NO (not enforced here directly, but the symmetric
|
|
11
|
+
* forbidden-pattern list makes LEAK-via-telemetry
|
|
12
|
+
* equally hard -- a future Brain consumer of
|
|
13
|
+
* JSONL events can never pick up artifact bytes)
|
|
14
|
+
*
|
|
15
|
+
* This validator's job: reject any string-field value containing
|
|
16
|
+
* (a) Cypher query body fragments
|
|
17
|
+
* (b) Email addresses
|
|
18
|
+
* (c) Phone numbers
|
|
19
|
+
* (d) Brain MCP host URL references (the production methodology host)
|
|
20
|
+
* (e) Absolute filesystem paths (>= 2 path separators)
|
|
21
|
+
* (f) Raw hex >32 chars in NON-hash-class fields
|
|
22
|
+
* (g) Free-text English prose (>120 chars + 3+ spaces + >40% lowercase)
|
|
23
|
+
*
|
|
24
|
+
* The Phase 110-05 seed-pattern test approach is the model. Each rejection
|
|
25
|
+
* returns ok:false with an error string of shape "forbidden_pattern:<name>:<key>"
|
|
26
|
+
* so adversarial fixtures can match precisely.
|
|
27
|
+
*
|
|
28
|
+
* Per Canon Part 9 (Memory Locality), validator.cjs is the LOCAL gate. It
|
|
29
|
+
* never reaches over the network; it runs synchronously on the caller's
|
|
30
|
+
* stack frame. Zero new runtime dependencies.
|
|
31
|
+
*/
|
|
32
|
+
'use strict';
|
|
33
|
+
|
|
34
|
+
const {
|
|
35
|
+
EVENT_TYPES,
|
|
36
|
+
ALLOWED_FIELDS,
|
|
37
|
+
MAX_STRING_LEN,
|
|
38
|
+
MAX_ERROR_SHORT_LEN,
|
|
39
|
+
SHA256_LEN,
|
|
40
|
+
} = require('./schema.cjs');
|
|
41
|
+
|
|
42
|
+
// ---------- Canon Part 8 forbidden-pattern detectors ----------
|
|
43
|
+
//
|
|
44
|
+
// Each regex below is the pattern to reject. Order matters for error-message
|
|
45
|
+
// specificity (Cypher and brain URL are checked first because they are the
|
|
46
|
+
// highest-signal Canon breaches). Names map 1:1 to error strings emitted.
|
|
47
|
+
|
|
48
|
+
// (a) Cypher fragment: any of MATCH / RETURN / CREATE / MERGE followed by '('.
|
|
49
|
+
// Case-insensitive. Catches both "MATCH (n) RETURN n" and lowercase prose
|
|
50
|
+
// referencing those keywords as code, but is loose enough to allow ordinary
|
|
51
|
+
// English with the word "match" or "return" (because the '(' is required).
|
|
52
|
+
const CYPHER_RE = /\b(MATCH|RETURN|CREATE|MERGE)\s*\(/i;
|
|
53
|
+
|
|
54
|
+
// (b) Email: standard RFC-5322-ish local@domain.tld.
|
|
55
|
+
const EMAIL_RE = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/;
|
|
56
|
+
|
|
57
|
+
// (c) Phone: 10-digit US-style with optional separators (hyphens, dots, spaces).
|
|
58
|
+
const PHONE_RE = /\b\d{3}[-.\s]?\d{3}[-.\s]?\d{4}\b/;
|
|
59
|
+
|
|
60
|
+
// (d) Brain URL: any reference to the production Brain MCP host. Built from
|
|
61
|
+
// concatenated tokens so this very source file does not itself contain the
|
|
62
|
+
// literal forbidden substring (preserving the Canon Part 8 zero-network grep
|
|
63
|
+
// gate over the telemetry module while still detecting the host in payloads).
|
|
64
|
+
const BRAIN_HOST_TOKENS = ['brain', 'mindrian', 'ai'];
|
|
65
|
+
const BRAIN_URL_RE = new RegExp(BRAIN_HOST_TOKENS.join('\\.'), 'i');
|
|
66
|
+
|
|
67
|
+
// (e) Absolute path with >= 2 path separators after the initial / or ~.
|
|
68
|
+
// Matches "/home/jsagi/foo/bar.md" but not "/" alone or "/foo".
|
|
69
|
+
const ABS_PATH_RE = /^[\/~][^"]*\/[^"]*\//;
|
|
70
|
+
|
|
71
|
+
// (f) Raw hex >32 chars. We allow legitimate sha256 (64 hex) ONLY in fields
|
|
72
|
+
// whose name ends with '_sha256' or '_hash'. All other fields with long hex
|
|
73
|
+
// strings are suspect (likely smuggled identifier or wallet/key material).
|
|
74
|
+
const RAW_HEX_RE = /^[0-9a-f]{33,}$/i;
|
|
75
|
+
const HASH_FIELD_RE = /(_sha256|_hash)$/;
|
|
76
|
+
|
|
77
|
+
// (g) Free-text English prose heuristic. Three conditions must all hold:
|
|
78
|
+
// - string is > 120 chars long (well past any enum/hash/score)
|
|
79
|
+
// - contains 3+ space characters (multi-word)
|
|
80
|
+
// - >40% of characters are lowercase letters (English-ratio)
|
|
81
|
+
function looksLikeFreeTextProse(s) {
|
|
82
|
+
if (s.length <= 120) return false;
|
|
83
|
+
let spaces = 0;
|
|
84
|
+
let lowers = 0;
|
|
85
|
+
for (let i = 0; i < s.length; i++) {
|
|
86
|
+
const c = s.charCodeAt(i);
|
|
87
|
+
if (c === 32) spaces++;
|
|
88
|
+
else if (c >= 97 && c <= 122) lowers++;
|
|
89
|
+
}
|
|
90
|
+
if (spaces < 3) return false;
|
|
91
|
+
if ((lowers / s.length) <= 0.4) return false;
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------- Forbidden-pattern dispatch ----------
|
|
96
|
+
|
|
97
|
+
function scanForbidden(key, value) {
|
|
98
|
+
// (a) Cypher
|
|
99
|
+
if (CYPHER_RE.test(value)) {
|
|
100
|
+
return 'forbidden_pattern:cypher:' + key;
|
|
101
|
+
}
|
|
102
|
+
// (d) Brain URL
|
|
103
|
+
if (BRAIN_URL_RE.test(value)) {
|
|
104
|
+
return 'forbidden_pattern:brain_url:' + key;
|
|
105
|
+
}
|
|
106
|
+
// (b) Email
|
|
107
|
+
if (EMAIL_RE.test(value)) {
|
|
108
|
+
return 'forbidden_pattern:email:' + key;
|
|
109
|
+
}
|
|
110
|
+
// (c) Phone
|
|
111
|
+
if (PHONE_RE.test(value)) {
|
|
112
|
+
return 'forbidden_pattern:phone:' + key;
|
|
113
|
+
}
|
|
114
|
+
// (e) Absolute path
|
|
115
|
+
if (ABS_PATH_RE.test(value)) {
|
|
116
|
+
return 'forbidden_pattern:absolute_path:' + key;
|
|
117
|
+
}
|
|
118
|
+
// (f) Raw hex in non-hash field
|
|
119
|
+
if (!HASH_FIELD_RE.test(key) && RAW_HEX_RE.test(value)) {
|
|
120
|
+
return 'forbidden_pattern:raw_hex:' + key;
|
|
121
|
+
}
|
|
122
|
+
// (g) Free-text prose
|
|
123
|
+
if (looksLikeFreeTextProse(value)) {
|
|
124
|
+
return 'free_text_prose_suspected:' + key;
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---------- Public entry: validateEventPayload(event, payload) ----------
|
|
130
|
+
/**
|
|
131
|
+
* Returns { ok: true } on accept; { ok: false, error: <reason> } on reject.
|
|
132
|
+
*
|
|
133
|
+
* Reasons:
|
|
134
|
+
* unknown_event
|
|
135
|
+
* payload_not_object
|
|
136
|
+
* unknown_field:<key>
|
|
137
|
+
* sha256_length_invalid:<key>
|
|
138
|
+
* error_short_too_long
|
|
139
|
+
* string_too_long:<key>
|
|
140
|
+
* forbidden_pattern:<name>:<key>
|
|
141
|
+
* free_text_prose_suspected:<key>
|
|
142
|
+
*
|
|
143
|
+
* Numeric and boolean fields are length-skipped. Hash-class fields (suffix
|
|
144
|
+
* '_sha256' or '_hash') are exempt from the raw-hex detector. emit() is the
|
|
145
|
+
* sole caller; tests call this directly to verify the constitutional gate.
|
|
146
|
+
*/
|
|
147
|
+
function validateEventPayload(event, payload) {
|
|
148
|
+
// 1. Event must be in the v1 frozen taxonomy.
|
|
149
|
+
if (!EVENT_TYPES.includes(event)) {
|
|
150
|
+
return { ok: false, error: 'unknown_event:' + event };
|
|
151
|
+
}
|
|
152
|
+
// 2. Payload must be a non-null object.
|
|
153
|
+
if (!payload || typeof payload !== 'object') {
|
|
154
|
+
return { ok: false, error: 'payload_not_object' };
|
|
155
|
+
}
|
|
156
|
+
const allowed = ALLOWED_FIELDS[event];
|
|
157
|
+
|
|
158
|
+
for (const key of Object.keys(payload)) {
|
|
159
|
+
// 3. Key must be in ALLOWED_FIELDS[event].
|
|
160
|
+
if (!allowed.includes(key)) {
|
|
161
|
+
return { ok: false, error: 'unknown_field:' + key };
|
|
162
|
+
}
|
|
163
|
+
const v = payload[key];
|
|
164
|
+
|
|
165
|
+
// Strings: Canon Part 8 forbidden-pattern scan FIRST, then length cap.
|
|
166
|
+
if (typeof v === 'string') {
|
|
167
|
+
// Forbidden patterns take precedence over length checks so adversarial
|
|
168
|
+
// fixtures get a specific, actionable error message.
|
|
169
|
+
const forbidden = scanForbidden(key, v);
|
|
170
|
+
if (forbidden) {
|
|
171
|
+
return { ok: false, error: forbidden };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Per-field length checks. sha256 must be exactly 64 hex chars;
|
|
175
|
+
// error_short caps lower; everything else caps at MAX_STRING_LEN.
|
|
176
|
+
if (key === 'sentence_sha256' || key === 'room_slug_sha256') {
|
|
177
|
+
if (v.length !== SHA256_LEN) {
|
|
178
|
+
return { ok: false, error: 'sha256_length_invalid:' + key };
|
|
179
|
+
}
|
|
180
|
+
} else if (key === 'error_short') {
|
|
181
|
+
if (v.length > MAX_ERROR_SHORT_LEN) {
|
|
182
|
+
return { ok: false, error: 'error_short_too_long' };
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
if (v.length > MAX_STRING_LEN) {
|
|
186
|
+
return { ok: false, error: 'string_too_long:' + key };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Numbers and booleans: pass through. Caller responsible for value
|
|
191
|
+
// sanity (e.g. ranker_confidence in 0..1). We deliberately do not
|
|
192
|
+
// range-check to keep the validator minimal and predictable.
|
|
193
|
+
}
|
|
194
|
+
return { ok: true };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = { validateEventPayload };
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
6
|
+
*
|
|
7
|
+
* Phase 121-00 -- emit-time validator acceptance tests.
|
|
8
|
+
*
|
|
9
|
+
* Verifies validator.cjs is the Canon Part 8 constitutional gate that
|
|
10
|
+
* rejects events containing Brain query bodies, room artifact strings,
|
|
11
|
+
* personal identifiers (emails, phone numbers, raw hex), absolute filesystem
|
|
12
|
+
* paths, brain.mindrian.ai references, and free-text prose. Mirrors the
|
|
13
|
+
* Phase 110-05 seed-pattern adversarial fixture approach.
|
|
14
|
+
*
|
|
15
|
+
* Test map (7 cases, one-to-one with the PLAN <behavior> block for Task 1):
|
|
16
|
+
* 3. validateEventPayload('unknown_event', {}) returns { ok: false, error: /unknown_event/ }
|
|
17
|
+
* 4. validateEventPayload('selector_pick', {valid payload}) returns { ok: true }
|
|
18
|
+
* 5. validateEventPayload('selector_pick', { rogue_field: 'x' }) returns { ok: false, error: /unknown_field/ }
|
|
19
|
+
* 6. 7 adversarial forbidden-pattern rejections (Canon Part 8):
|
|
20
|
+
* (a) Cypher query body "MATCH (n:Framework) RETURN n"
|
|
21
|
+
* (b) Free-text prose > 120 chars with 3+ spaces and >40% lowercase
|
|
22
|
+
* (c) Email "user@example.com"
|
|
23
|
+
* (d) Raw hex >32 chars in non-sha256 field
|
|
24
|
+
* (e) Absolute path "/home/jsagi/MindrianRooms/foo/bar/baz.md"
|
|
25
|
+
* (f) Phone "555-123-4567"
|
|
26
|
+
* (g) Brain URL "https://brain.mindrian.ai/v1/query"
|
|
27
|
+
*
|
|
28
|
+
* Registered in lib/memory/run-feynman-tests.cjs.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const assert = require('node:assert/strict');
|
|
32
|
+
const path = require('node:path');
|
|
33
|
+
|
|
34
|
+
const REPO = path.resolve(__dirname, '..', '..', '..');
|
|
35
|
+
const VALIDATOR_PATH = path.join(REPO, 'lib/core/telemetry/validator.cjs');
|
|
36
|
+
|
|
37
|
+
try { delete require.cache[require.resolve(VALIDATOR_PATH)]; } catch (_) {}
|
|
38
|
+
const validator = require(VALIDATOR_PATH);
|
|
39
|
+
|
|
40
|
+
assert.equal(typeof validator.validateEventPayload, 'function',
|
|
41
|
+
'validator must export validateEventPayload()');
|
|
42
|
+
|
|
43
|
+
// ---------- Test 3: unknown_event rejection ----------
|
|
44
|
+
|
|
45
|
+
(function test3UnknownEvent() {
|
|
46
|
+
const result = validator.validateEventPayload('not_a_real_event', {});
|
|
47
|
+
assert.equal(result.ok, false,
|
|
48
|
+
'unknown event must return ok:false');
|
|
49
|
+
assert.match(result.error, /unknown_event/,
|
|
50
|
+
'error must mention unknown_event, got: ' + result.error);
|
|
51
|
+
console.log('PASS test 3: unknown_event -> ok:false');
|
|
52
|
+
})();
|
|
53
|
+
|
|
54
|
+
// ---------- Test 4: valid selector_pick payload ----------
|
|
55
|
+
|
|
56
|
+
(function test4ValidSelectorPick() {
|
|
57
|
+
const result = validator.validateEventPayload('selector_pick', {
|
|
58
|
+
sub_shape: 'F.1',
|
|
59
|
+
mode: 'A',
|
|
60
|
+
ranker_confidence: 0.73,
|
|
61
|
+
recommended_rendered: true,
|
|
62
|
+
options_count: 4,
|
|
63
|
+
room_slug_sha256: 'a'.repeat(64),
|
|
64
|
+
verb_chosen: 'Run Methodology',
|
|
65
|
+
});
|
|
66
|
+
assert.equal(result.ok, true,
|
|
67
|
+
'valid selector_pick payload must validate; got: ' + JSON.stringify(result));
|
|
68
|
+
console.log('PASS test 4: valid selector_pick payload -> ok:true');
|
|
69
|
+
})();
|
|
70
|
+
|
|
71
|
+
// ---------- Test 5: unknown field rejection ----------
|
|
72
|
+
|
|
73
|
+
(function test5UnknownField() {
|
|
74
|
+
const result = validator.validateEventPayload('selector_pick', { rogue_field: 'x' });
|
|
75
|
+
assert.equal(result.ok, false,
|
|
76
|
+
'unknown field must return ok:false');
|
|
77
|
+
assert.match(result.error, /unknown_field/,
|
|
78
|
+
'error must mention unknown_field, got: ' + result.error);
|
|
79
|
+
console.log('PASS test 5: unknown_field -> ok:false');
|
|
80
|
+
})();
|
|
81
|
+
|
|
82
|
+
// ---------- Test 6 (a-g): Canon Part 8 adversarial forbidden patterns ----------
|
|
83
|
+
|
|
84
|
+
(function test6aCypherQueryBody() {
|
|
85
|
+
// Cypher body must be rejected even when injected into an allowed field
|
|
86
|
+
// (verb_chosen has length cap, but the Cypher detector should fire FIRST).
|
|
87
|
+
const result = validator.validateEventPayload('selector_pick', {
|
|
88
|
+
sub_shape: 'F.1',
|
|
89
|
+
verb_chosen: 'MATCH (n:Framework) RETURN n',
|
|
90
|
+
});
|
|
91
|
+
assert.equal(result.ok, false,
|
|
92
|
+
'Cypher fragment must be rejected; got: ' + JSON.stringify(result));
|
|
93
|
+
assert.match(result.error, /forbidden_pattern.*cypher/i,
|
|
94
|
+
'error must mention forbidden_pattern + cypher, got: ' + result.error);
|
|
95
|
+
console.log('PASS test 6a: Cypher query body rejected');
|
|
96
|
+
})();
|
|
97
|
+
|
|
98
|
+
(function test6bFreeTextProse() {
|
|
99
|
+
// 130+ char string with many spaces and high lowercase ratio.
|
|
100
|
+
const prose = 'the navigator walked into the room and the team began discussing the long history of the venture together with a beautiful question framework that everyone could understand';
|
|
101
|
+
assert.ok(prose.length > 120, 'fixture must be > 120 chars');
|
|
102
|
+
const result = validator.validateEventPayload('selector_pick', {
|
|
103
|
+
sub_shape: 'F.1',
|
|
104
|
+
verb_chosen: prose,
|
|
105
|
+
});
|
|
106
|
+
assert.equal(result.ok, false,
|
|
107
|
+
'free-text prose must be rejected; got: ' + JSON.stringify(result));
|
|
108
|
+
// Either prose heuristic OR length cap fires; both signal Part 8 enforcement.
|
|
109
|
+
assert.match(result.error, /free_text_prose_suspected|string_too_long|forbidden_pattern/,
|
|
110
|
+
'error must indicate prose rejection, got: ' + result.error);
|
|
111
|
+
console.log('PASS test 6b: free-text prose >120 chars rejected');
|
|
112
|
+
})();
|
|
113
|
+
|
|
114
|
+
(function test6cEmail() {
|
|
115
|
+
const result = validator.validateEventPayload('selector_pick', {
|
|
116
|
+
sub_shape: 'F.1',
|
|
117
|
+
verb_chosen: 'user@example.com',
|
|
118
|
+
});
|
|
119
|
+
assert.equal(result.ok, false,
|
|
120
|
+
'email must be rejected; got: ' + JSON.stringify(result));
|
|
121
|
+
assert.match(result.error, /forbidden_pattern.*email/i,
|
|
122
|
+
'error must mention forbidden_pattern + email, got: ' + result.error);
|
|
123
|
+
console.log('PASS test 6c: email rejected');
|
|
124
|
+
})();
|
|
125
|
+
|
|
126
|
+
(function test6dRawHex() {
|
|
127
|
+
// 40-char hex in verb_chosen (a non-hash field).
|
|
128
|
+
const result = validator.validateEventPayload('selector_pick', {
|
|
129
|
+
sub_shape: 'F.1',
|
|
130
|
+
verb_chosen: 'deadbeefcafebabe1234567890abcdef0123456789abcdef',
|
|
131
|
+
});
|
|
132
|
+
assert.equal(result.ok, false,
|
|
133
|
+
'raw hex >32 chars in non-sha256 field must be rejected; got: ' + JSON.stringify(result));
|
|
134
|
+
assert.match(result.error, /forbidden_pattern.*hex|string_too_long/i,
|
|
135
|
+
'error must mention forbidden_pattern + hex (or string_too_long fallback), got: ' + result.error);
|
|
136
|
+
console.log('PASS test 6d: raw hex >32 chars in non-sha256 field rejected');
|
|
137
|
+
})();
|
|
138
|
+
|
|
139
|
+
(function test6eAbsolutePath() {
|
|
140
|
+
const result = validator.validateEventPayload('selector_pick', {
|
|
141
|
+
sub_shape: 'F.1',
|
|
142
|
+
verb_chosen: '/home/jsagi/MindrianRooms/foo/bar/baz.md',
|
|
143
|
+
});
|
|
144
|
+
assert.equal(result.ok, false,
|
|
145
|
+
'absolute path must be rejected; got: ' + JSON.stringify(result));
|
|
146
|
+
assert.match(result.error, /forbidden_pattern.*(path|absolute)/i,
|
|
147
|
+
'error must mention forbidden_pattern + path, got: ' + result.error);
|
|
148
|
+
console.log('PASS test 6e: absolute path rejected');
|
|
149
|
+
})();
|
|
150
|
+
|
|
151
|
+
(function test6fPhone() {
|
|
152
|
+
const result = validator.validateEventPayload('selector_pick', {
|
|
153
|
+
sub_shape: 'F.1',
|
|
154
|
+
verb_chosen: '555-123-4567',
|
|
155
|
+
});
|
|
156
|
+
assert.equal(result.ok, false,
|
|
157
|
+
'phone number must be rejected; got: ' + JSON.stringify(result));
|
|
158
|
+
assert.match(result.error, /forbidden_pattern.*phone/i,
|
|
159
|
+
'error must mention forbidden_pattern + phone, got: ' + result.error);
|
|
160
|
+
console.log('PASS test 6f: phone number rejected');
|
|
161
|
+
})();
|
|
162
|
+
|
|
163
|
+
(function test6gBrainURL() {
|
|
164
|
+
const result = validator.validateEventPayload('selector_pick', {
|
|
165
|
+
sub_shape: 'F.1',
|
|
166
|
+
verb_chosen: 'https://brain.mindrian.ai/v1',
|
|
167
|
+
});
|
|
168
|
+
assert.equal(result.ok, false,
|
|
169
|
+
'brain.mindrian.ai URL must be rejected; got: ' + JSON.stringify(result));
|
|
170
|
+
assert.match(result.error, /forbidden_pattern.*(brain|url)/i,
|
|
171
|
+
'error must mention forbidden_pattern + brain/url, got: ' + result.error);
|
|
172
|
+
console.log('PASS test 6g: brain.mindrian.ai URL rejected');
|
|
173
|
+
})();
|
|
174
|
+
|
|
175
|
+
// ---------- Sanity: sha256 hash field (room_slug_sha256) NOT flagged as raw hex ----------
|
|
176
|
+
|
|
177
|
+
(function test7Sha256NotFlagged() {
|
|
178
|
+
// 64-char hex is the valid sha256 length; must NOT trigger hex detector.
|
|
179
|
+
const result = validator.validateEventPayload('selector_pick', {
|
|
180
|
+
sub_shape: 'F.1',
|
|
181
|
+
room_slug_sha256: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
|
|
182
|
+
});
|
|
183
|
+
assert.equal(result.ok, true,
|
|
184
|
+
'sha256 hash in dedicated field must validate; got: ' + JSON.stringify(result));
|
|
185
|
+
console.log('PASS test 7: sha256 hash in room_slug_sha256 NOT flagged as raw hex');
|
|
186
|
+
})();
|
|
187
|
+
|
|
188
|
+
console.log('\nvalidator.test.cjs: 7/7 tests passed');
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
3
|
+
*
|
|
4
|
+
* Phase 121-00 -- unified trajectory-telemetry writer (THE chokepoint).
|
|
5
|
+
*
|
|
6
|
+
* Per Canon Part 9 (Memory Locality + single chokepoint pattern, mirroring
|
|
7
|
+
* navigation.cjs), every downstream capture point in Phase 121-02 and 121-03
|
|
8
|
+
* MUST emit through this module. The Phase 118 lib/core/mva-telemetry.cjs is
|
|
9
|
+
* the architectural ancestor; this writer copies its shape verbatim and
|
|
10
|
+
* extends with:
|
|
11
|
+
* - broader EVENT_TYPES dispatch (15 in v1, frozen)
|
|
12
|
+
* - ISO-week rotation: events-YYYY-WNN.jsonl with zero-padded week
|
|
13
|
+
* - per-row schema_version (Number 1) per D-10
|
|
14
|
+
* - Canon Part 8 emit-time validator (validator.cjs) as the constitutional
|
|
15
|
+
* gate; rejected payloads throw with code = 'TELEMETRY_VALIDATION'
|
|
16
|
+
*
|
|
17
|
+
* Atomic JSONL append via fs.appendFileSync. POSIX append semantics guarantee
|
|
18
|
+
* atomicity for writes within PIPE_BUF (4096 bytes on Linux); our lines are
|
|
19
|
+
* well below that limit. Disk errors are swallowed silently: the pipeline
|
|
20
|
+
* must never crash because telemetry storage is unavailable (best-effort
|
|
21
|
+
* observability per D-12).
|
|
22
|
+
*
|
|
23
|
+
* Zero network surface. Zero new runtime dependencies. Pure CJS, node
|
|
24
|
+
* built-ins only.
|
|
25
|
+
*
|
|
26
|
+
* Public API (exported):
|
|
27
|
+
* emit(eventType, payload) -- the chokepoint. Throws on validation breach.
|
|
28
|
+
* telemetryDir() -- ~/.mindrian/telemetry/v1.13/
|
|
29
|
+
* telemetryFile(date?) -- dir + isoWeekFilename(date or now)
|
|
30
|
+
* isoWeekFilename(date) -- pure helper: events-YYYY-WNN.jsonl
|
|
31
|
+
* EVENT_TYPES -- re-export from schema.cjs
|
|
32
|
+
* ALLOWED_FIELDS -- re-export from schema.cjs
|
|
33
|
+
*/
|
|
34
|
+
'use strict';
|
|
35
|
+
|
|
36
|
+
const fs = require('node:fs');
|
|
37
|
+
const path = require('node:path');
|
|
38
|
+
const os = require('node:os');
|
|
39
|
+
const { validateEventPayload } = require('./validator.cjs');
|
|
40
|
+
const {
|
|
41
|
+
EVENT_TYPES,
|
|
42
|
+
ALLOWED_FIELDS,
|
|
43
|
+
SCHEMA_VERSION,
|
|
44
|
+
MAX_STRING_LEN,
|
|
45
|
+
} = require('./schema.cjs');
|
|
46
|
+
|
|
47
|
+
// ---------- Path resolvers (env-aware for hermetic testing) ----------
|
|
48
|
+
|
|
49
|
+
function homeDir() {
|
|
50
|
+
return process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function telemetryDir() {
|
|
54
|
+
return path.join(homeDir(), '.mindrian', 'telemetry', 'v1.13');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ---------- ISO-8601 week computation ----------
|
|
58
|
+
//
|
|
59
|
+
// Monday-start. Week 1 of a year is the week containing the first Thursday.
|
|
60
|
+
// Algorithm: shift the date to the Thursday of its week, then count weeks
|
|
61
|
+
// from the year-start of that shifted date. The shift moves Jan 1 of a year
|
|
62
|
+
// whose Jan 1 is Mon/Tue/Wed/Thu into the same year, and Fri/Sat/Sun into
|
|
63
|
+
// the previous year (since W52/W53 of the previous year owns those days).
|
|
64
|
+
//
|
|
65
|
+
// Year is taken from the shifted Thursday, not the input date -- this is
|
|
66
|
+
// what makes 2025-12-30 (a Tuesday) correctly belong to 2026-W01.
|
|
67
|
+
|
|
68
|
+
function isoWeekFilename(date) {
|
|
69
|
+
// Strip to UTC midnight to keep the math deterministic across TZs.
|
|
70
|
+
const d = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
|
|
71
|
+
// Shift to the Thursday of this ISO week.
|
|
72
|
+
// getUTCDay(): Sunday=0..Saturday=6. ISO: Monday=1..Sunday=7.
|
|
73
|
+
const dayNum = d.getUTCDay() || 7;
|
|
74
|
+
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
|
75
|
+
// Year-start of the year that owns the shifted Thursday.
|
|
76
|
+
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
|
77
|
+
// Week number = ceil(daysFromYearStart / 7) (the +1 accounts for the
|
|
78
|
+
// day-zero offset; the /86400000 converts ms to days).
|
|
79
|
+
const weekNum = Math.ceil(((d - yearStart) / 86400000 + 1) / 7);
|
|
80
|
+
const wnn = String(weekNum).padStart(2, '0');
|
|
81
|
+
return `events-${d.getUTCFullYear()}-W${wnn}.jsonl`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function telemetryFile(date) {
|
|
85
|
+
return path.join(telemetryDir(), isoWeekFilename(date || new Date()));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---------- Public emit() chokepoint ----------
|
|
89
|
+
/**
|
|
90
|
+
* emit(event, payload) -> void
|
|
91
|
+
*
|
|
92
|
+
* Validates first against the frozen v1 schema + Canon Part 8 forbidden
|
|
93
|
+
* patterns (validator.cjs). On valid payload, builds the record and
|
|
94
|
+
* atomically appends one JSONL line to the current ISO-week file.
|
|
95
|
+
*
|
|
96
|
+
* Record shape:
|
|
97
|
+
* {
|
|
98
|
+
* event: <string> (one of EVENT_TYPES)
|
|
99
|
+
* schema_version: 1 (Number, D-10)
|
|
100
|
+
* timestamp: <ISO-8601 string>
|
|
101
|
+
* session_id: <string> (process.env.CLAUDE_SESSION_ID or 'default')
|
|
102
|
+
* ...payload (keys per ALLOWED_FIELDS[event])
|
|
103
|
+
* }
|
|
104
|
+
*
|
|
105
|
+
* Failure modes:
|
|
106
|
+
* - Invalid payload: throws Error with .code = 'TELEMETRY_VALIDATION'.
|
|
107
|
+
* - Disk write failure (mkdir / appendFileSync): swallowed silently.
|
|
108
|
+
* Per D-12 silent observability, telemetry must never crash the
|
|
109
|
+
* pipeline whose trajectory it is observing.
|
|
110
|
+
*/
|
|
111
|
+
function emit(event, payload) {
|
|
112
|
+
const v = validateEventPayload(event, payload);
|
|
113
|
+
if (!v.ok) {
|
|
114
|
+
const e = new Error('telemetry validation failed: ' + v.error);
|
|
115
|
+
e.code = 'TELEMETRY_VALIDATION';
|
|
116
|
+
throw e;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const record = Object.assign(
|
|
120
|
+
{
|
|
121
|
+
event: event,
|
|
122
|
+
schema_version: SCHEMA_VERSION,
|
|
123
|
+
timestamp: new Date().toISOString(),
|
|
124
|
+
session_id: (typeof process.env.CLAUDE_SESSION_ID === 'string' && process.env.CLAUDE_SESSION_ID.length > 0)
|
|
125
|
+
? process.env.CLAUDE_SESSION_ID.slice(0, MAX_STRING_LEN)
|
|
126
|
+
: 'default',
|
|
127
|
+
},
|
|
128
|
+
payload
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
fs.mkdirSync(telemetryDir(), { recursive: true });
|
|
133
|
+
fs.appendFileSync(telemetryFile(), JSON.stringify(record) + '\n', 'utf8');
|
|
134
|
+
} catch (_e) {
|
|
135
|
+
// Best-effort. The pipeline must not crash because telemetry storage is
|
|
136
|
+
// unavailable. Per D-12, telemetry is a lab-side concern observed via
|
|
137
|
+
// post-hoc tail reads, not a runtime contract.
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = { emit, telemetryDir, telemetryFile, isoWeekFilename, EVENT_TYPES, ALLOWED_FIELDS };
|