@mindrian_os/install 1.13.0-beta.16 → 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.
Files changed (219) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +36 -0
  3. package/commands/act.md +1 -0
  4. package/commands/admin.md +1 -0
  5. package/commands/analyze-needs.md +2 -0
  6. package/commands/analyze-systems.md +2 -0
  7. package/commands/analyze-timing.md +2 -0
  8. package/commands/auto-explore.md +2 -0
  9. package/commands/beautiful-question.md +2 -0
  10. package/commands/brain-derive.md +2 -0
  11. package/commands/build-knowledge.md +2 -0
  12. package/commands/build-thesis.md +2 -0
  13. package/commands/causal.md +2 -0
  14. package/commands/challenge-assumptions.md +2 -0
  15. package/commands/compare-ventures.md +2 -0
  16. package/commands/dashboard.md +2 -1
  17. package/commands/deep-grade.md +2 -0
  18. package/commands/diagnose.md +21 -1
  19. package/commands/diagnostics.md +14 -3
  20. package/commands/doctor.md +4 -1
  21. package/commands/dogfood-flush.md +92 -0
  22. package/commands/dominant-designs.md +2 -0
  23. package/commands/explain-decision.md +2 -0
  24. package/commands/explore-domains.md +2 -0
  25. package/commands/explore-futures.md +2 -0
  26. package/commands/explore-trends.md +2 -0
  27. package/commands/export.md +1 -0
  28. package/commands/feynman-timeline-refresh.md +2 -0
  29. package/commands/file-meeting.md +4 -0
  30. package/commands/find-analogies.md +1 -0
  31. package/commands/find-bottlenecks.md +2 -0
  32. package/commands/find-connections.md +2 -0
  33. package/commands/funding.md +1 -0
  34. package/commands/grade.md +4 -0
  35. package/commands/graph.md +1 -0
  36. package/commands/hat-briefing.md +1 -0
  37. package/commands/heal.md +22 -170
  38. package/commands/help.md +54 -334
  39. package/commands/hmi-status.md +23 -144
  40. package/commands/jtbd.md +1 -0
  41. package/commands/leadership.md +2 -0
  42. package/commands/lean-canvas.md +2 -0
  43. package/commands/macro-trends.md +2 -0
  44. package/commands/map-unknowns.md +2 -0
  45. package/commands/memory.md +1 -0
  46. package/commands/models.md +1 -0
  47. package/commands/mos-reason.md +2 -0
  48. package/commands/mos.md +139 -0
  49. package/commands/mullins.md +2 -0
  50. package/commands/mva-brief.md +58 -0
  51. package/commands/mva-option.md +91 -0
  52. package/commands/new-project.md +4 -0
  53. package/commands/onboard.md +22 -7
  54. package/commands/operator.md +1 -0
  55. package/commands/opportunities.md +1 -0
  56. package/commands/organize.md +22 -469
  57. package/commands/persona.md +1 -0
  58. package/commands/pipeline.md +2 -0
  59. package/commands/present.md +1 -0
  60. package/commands/publish.md +2 -0
  61. package/commands/query.md +24 -102
  62. package/commands/radar.md +2 -0
  63. package/commands/reanalyze.md +1 -0
  64. package/commands/research.md +2 -0
  65. package/commands/room.md +2 -0
  66. package/commands/rooms.md +1 -0
  67. package/commands/root-cause.md +2 -0
  68. package/commands/rs-experts.md +1 -0
  69. package/commands/rs-explain.md +1 -0
  70. package/commands/rs-fetch.md +1 -0
  71. package/commands/rs-thesis.md +1 -0
  72. package/commands/scenario-plan.md +2 -0
  73. package/commands/scheduled-tasks.md +1 -0
  74. package/commands/score-innovation.md +2 -0
  75. package/commands/scout.md +1 -0
  76. package/commands/setup.md +2 -0
  77. package/commands/snapshot.md +2 -0
  78. package/commands/speakers.md +1 -0
  79. package/commands/splash.md +5 -2
  80. package/commands/status.md +1 -0
  81. package/commands/structure-argument.md +2 -0
  82. package/commands/suggest-next.md +2 -0
  83. package/commands/systems-thinking.md +2 -0
  84. package/commands/think-hats.md +2 -0
  85. package/commands/update.md +2 -0
  86. package/commands/user-needs.md +2 -0
  87. package/commands/validate.md +2 -0
  88. package/commands/value-proposition.md +2 -0
  89. package/commands/vault.md +2 -0
  90. package/commands/visualize.md +24 -29
  91. package/commands/whitespace.md +2 -1
  92. package/commands/wiki.md +1 -0
  93. package/hooks/hooks.json +31 -88
  94. package/lib/agents/auto-explore-agent.cjs +82 -0
  95. package/lib/agents/mva/brain-classic-traps.cjs +77 -0
  96. package/lib/agents/mva/brain-cross-domain.cjs +79 -0
  97. package/lib/agents/mva/brain-similar-ventures.cjs +93 -0
  98. package/lib/agents/mva/dashboard-graph-neighborhood.cjs +72 -0
  99. package/lib/agents/mva/index.cjs +42 -0
  100. package/lib/agents/mva/six-hats-red-black.cjs +137 -0
  101. package/lib/agents/mva/tavily-funding-scan.cjs +147 -0
  102. package/lib/agents/mva/test-all-six-agents.cjs +467 -0
  103. package/lib/conversation/operator.cjs +64 -0
  104. package/lib/conversation/operator.test.cjs +160 -0
  105. package/lib/core/breakthrough/canary.cjs +134 -0
  106. package/lib/core/breakthrough/canary.test.cjs +136 -0
  107. package/lib/core/breakthrough/detectors.cjs +359 -0
  108. package/lib/core/breakthrough/detectors.test.cjs +333 -0
  109. package/lib/core/breakthrough/ethics-fence.cjs +127 -0
  110. package/lib/core/breakthrough/ethics-fence.test.cjs +178 -0
  111. package/lib/core/breakthrough/resurfacing.cjs +150 -0
  112. package/lib/core/breakthrough/resurfacing.test.cjs +233 -0
  113. package/lib/core/breakthrough/review-queue.cjs +154 -0
  114. package/lib/core/breakthrough/review-queue.test.cjs +160 -0
  115. package/lib/core/breakthrough/scanner-d17-d18.test.cjs +229 -0
  116. package/lib/core/breakthrough/scanner.cjs +426 -0
  117. package/lib/core/breakthrough/scanner.test.cjs +267 -0
  118. package/lib/core/breakthrough/schema.cjs +164 -0
  119. package/lib/core/breakthrough/schema.test.cjs +256 -0
  120. package/lib/core/breakthrough/scoring.cjs +293 -0
  121. package/lib/core/breakthrough/scoring.test.cjs +423 -0
  122. package/lib/core/breakthrough/verb-dispatch.cjs +221 -0
  123. package/lib/core/breakthrough/verb-dispatch.test.cjs +185 -0
  124. package/lib/core/breakthrough/voice-scaffold.cjs +247 -0
  125. package/lib/core/breakthrough/voice-scaffold.test.cjs +251 -0
  126. package/lib/core/first-touch-version-stamper.cjs +113 -0
  127. package/lib/core/larry-thinness-acknowledgment.cjs +64 -0
  128. package/lib/core/larry-thinness-acknowledgment.test.cjs +97 -0
  129. package/lib/core/llm-name-suggester.cjs +194 -0
  130. package/lib/core/llm-name-suggester.test.cjs +132 -0
  131. package/lib/core/mva-agent-contract.cjs +170 -0
  132. package/lib/core/mva-agent-contract.test.cjs +169 -0
  133. package/lib/core/mva-budget.cjs +75 -0
  134. package/lib/core/mva-budget.test.cjs +68 -0
  135. package/lib/core/mva-classifier.cjs +370 -0
  136. package/lib/core/mva-classifier.test.cjs +248 -0
  137. package/lib/core/mva-deck-builder.cjs +452 -0
  138. package/lib/core/mva-deck-builder.test.cjs +287 -0
  139. package/lib/core/mva-detect.smoke.test.cjs +197 -0
  140. package/lib/core/mva-dispatcher.cjs +110 -0
  141. package/lib/core/mva-dispatcher.test.cjs +216 -0
  142. package/lib/core/mva-option-router.cjs +292 -0
  143. package/lib/core/mva-option-router.test.cjs +483 -0
  144. package/lib/core/mva-orchestrator.cjs +365 -0
  145. package/lib/core/mva-orchestrator.test.cjs +908 -0
  146. package/lib/core/mva-progressive-renderer.cjs +194 -0
  147. package/lib/core/mva-progressive-renderer.test.cjs +157 -0
  148. package/lib/core/mva-rule-linter.cjs +213 -0
  149. package/lib/core/mva-rule-linter.test.cjs +336 -0
  150. package/lib/core/mva-state.cjs +159 -0
  151. package/lib/core/mva-telemetry.cjs +58 -0
  152. package/lib/core/mva-telemetry.test.cjs +196 -0
  153. package/lib/core/mva-vercel-deploy.cjs +168 -0
  154. package/lib/core/mva-vercel-deploy.test.cjs +239 -0
  155. package/lib/core/navigation/dashboard-helpers.cjs +145 -0
  156. package/lib/core/navigation/edges.cjs +35 -0
  157. package/lib/core/navigation/memory-events.cjs +126 -0
  158. package/lib/core/navigation.cjs +11 -0
  159. package/lib/core/resolve-vercel-key.cjs +107 -0
  160. package/lib/core/resolve-vercel-key.test.cjs +137 -0
  161. package/lib/core/room-auto-create.cjs +318 -0
  162. package/lib/core/room-auto-create.test.cjs +198 -0
  163. package/lib/core/room-discard-cascade.cjs +225 -0
  164. package/lib/core/room-discard-cascade.test.cjs +135 -0
  165. package/lib/core/room-name-validator.cjs +132 -0
  166. package/lib/core/room-name-validator.test.cjs +156 -0
  167. package/lib/core/room-naming-selector.cjs +357 -0
  168. package/lib/core/room-naming-selector.test.cjs +277 -0
  169. package/lib/core/room-receipt-emit.cjs +63 -0
  170. package/lib/core/room-skeleton-scaffold.cjs +315 -0
  171. package/lib/core/room-skeleton-scaffold.test.cjs +291 -0
  172. package/lib/core/stale-copy-scanner.cjs +190 -0
  173. package/lib/core/state-aware-router.cjs +78 -0
  174. package/lib/core/telemetry/schema.cjs +168 -0
  175. package/lib/core/telemetry/schema.test.cjs +124 -0
  176. package/lib/core/telemetry/validator.cjs +197 -0
  177. package/lib/core/telemetry/validator.test.cjs +188 -0
  178. package/lib/core/telemetry/writer.cjs +141 -0
  179. package/lib/core/telemetry/writer.test.cjs +331 -0
  180. package/lib/core/terminal-capability.cjs +88 -0
  181. package/lib/core/venture-shape-nudge.cjs +163 -0
  182. package/lib/core/venture-shape-nudge.test.cjs +161 -0
  183. package/lib/core/visual-ops.cjs +70 -2
  184. package/lib/hmi/selector-dispatcher.cjs +90 -1
  185. package/lib/hmi/shape-f7-breakthrough-renderer.cjs +222 -0
  186. package/lib/hmi/shape-f7-breakthrough-renderer.test.cjs +233 -0
  187. package/lib/memory/body-shape-coverage.test.cjs +268 -0
  188. package/lib/memory/doctor-deprecation-surface.test.cjs +185 -0
  189. package/lib/memory/first-touch-version.test.cjs +198 -0
  190. package/lib/memory/help-coverage.test.cjs +108 -0
  191. package/lib/memory/help-renderer.test.cjs +145 -0
  192. package/lib/memory/palette-consistency.test.cjs +127 -0
  193. package/lib/memory/pending-tension-store.cjs +80 -0
  194. package/lib/memory/render-v2-disposition.test.cjs +199 -0
  195. package/lib/memory/run-feynman-tests.cjs +240 -0
  196. package/lib/memory/sessionstart-coordinator.test.cjs +446 -0
  197. package/lib/memory/skill-vs-code-drift.test.cjs +257 -0
  198. package/lib/memory/soft-alias.test.cjs +144 -0
  199. package/lib/memory/stale-copy-scanner.test.cjs +291 -0
  200. package/lib/memory/state-aware-router.test.cjs +90 -0
  201. package/lib/memory/statusline-two-row.test.cjs +338 -0
  202. package/lib/memory/terminal-capability.test.cjs +155 -0
  203. package/lib/render/ROOM.md +74 -22
  204. package/lib/sessionstart/budget-compressor.cjs +130 -0
  205. package/lib/sessionstart/contributor-interface.cjs +134 -0
  206. package/lib/sessionstart/contributor-isolator.cjs +128 -0
  207. package/lib/sessionstart/precedence-ladder.cjs +47 -0
  208. package/lib/statusline/governing-thought-truncator.cjs +45 -0
  209. package/lib/statusline/two-row-renderer.cjs +186 -0
  210. package/lib/statusline/version-resolver.cjs +81 -0
  211. package/package.json +1 -1
  212. package/references/visual/ROOM.md +55 -0
  213. package/references/visual/palette.json +54 -0
  214. package/skills/larry-personality/SKILL.md +34 -0
  215. package/skills/mva-pipeline/SKILL.md +129 -0
  216. package/skills/ui-system/SKILL.md +109 -1
  217. package/skills/ui-system/rules/dual-palette.md +156 -0
  218. package/skills/ui-system/rules/glyph-disambiguation.md +171 -0
  219. 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 };