@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.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +36 -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 +4 -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 +4 -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 +58 -0
- package/commands/mva-option.md +91 -0
- package/commands/new-project.md +4 -0
- package/commands/onboard.md +22 -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 +31 -88
- package/lib/agents/auto-explore-agent.cjs +82 -0
- package/lib/agents/mva/brain-classic-traps.cjs +77 -0
- package/lib/agents/mva/brain-cross-domain.cjs +79 -0
- package/lib/agents/mva/brain-similar-ventures.cjs +93 -0
- package/lib/agents/mva/dashboard-graph-neighborhood.cjs +72 -0
- package/lib/agents/mva/index.cjs +42 -0
- package/lib/agents/mva/six-hats-red-black.cjs +137 -0
- package/lib/agents/mva/tavily-funding-scan.cjs +147 -0
- package/lib/agents/mva/test-all-six-agents.cjs +467 -0
- package/lib/conversation/operator.cjs +64 -0
- package/lib/conversation/operator.test.cjs +160 -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-agent-contract.cjs +170 -0
- package/lib/core/mva-agent-contract.test.cjs +169 -0
- package/lib/core/mva-budget.cjs +75 -0
- package/lib/core/mva-budget.test.cjs +68 -0
- package/lib/core/mva-classifier.cjs +370 -0
- package/lib/core/mva-classifier.test.cjs +248 -0
- package/lib/core/mva-deck-builder.cjs +452 -0
- package/lib/core/mva-deck-builder.test.cjs +287 -0
- package/lib/core/mva-detect.smoke.test.cjs +197 -0
- package/lib/core/mva-dispatcher.cjs +110 -0
- package/lib/core/mva-dispatcher.test.cjs +216 -0
- package/lib/core/mva-option-router.cjs +292 -0
- package/lib/core/mva-option-router.test.cjs +483 -0
- package/lib/core/mva-orchestrator.cjs +365 -0
- package/lib/core/mva-orchestrator.test.cjs +908 -0
- package/lib/core/mva-progressive-renderer.cjs +194 -0
- package/lib/core/mva-progressive-renderer.test.cjs +157 -0
- package/lib/core/mva-rule-linter.cjs +213 -0
- package/lib/core/mva-rule-linter.test.cjs +336 -0
- package/lib/core/mva-state.cjs +159 -0
- package/lib/core/mva-telemetry.cjs +58 -0
- package/lib/core/mva-telemetry.test.cjs +196 -0
- package/lib/core/mva-vercel-deploy.cjs +168 -0
- package/lib/core/mva-vercel-deploy.test.cjs +239 -0
- package/lib/core/navigation/dashboard-helpers.cjs +145 -0
- package/lib/core/navigation/edges.cjs +35 -0
- package/lib/core/navigation/memory-events.cjs +126 -0
- package/lib/core/navigation.cjs +11 -0
- package/lib/core/resolve-vercel-key.cjs +107 -0
- package/lib/core/resolve-vercel-key.test.cjs +137 -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 +240 -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/mva-pipeline/SKILL.md +129 -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,331 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
6
|
+
*
|
|
7
|
+
* Phase 121-00 -- unified writer acceptance tests.
|
|
8
|
+
*
|
|
9
|
+
* Verifies lib/core/telemetry/writer.cjs is THE single chokepoint (Canon
|
|
10
|
+
* Part 9 navigation.cjs precedent) for trajectory-telemetry emits. Every
|
|
11
|
+
* downstream capture point in 121-02 and 121-03 routes through emit() here.
|
|
12
|
+
*
|
|
13
|
+
* Test map (10 cases, one-to-one with the PLAN <behavior> block for Task 2):
|
|
14
|
+
* 1. Writer exports surface: emit, telemetryDir, telemetryFile,
|
|
15
|
+
* isoWeekFilename, EVENT_TYPES, ALLOWED_FIELDS.
|
|
16
|
+
* 2. telemetryDir() resolves under $HOME/.mindrian/telemetry/v1.13.
|
|
17
|
+
* 3. isoWeekFilename() zero-pads week and gates 2026-01-01 -> W01,
|
|
18
|
+
* 2026-01-05 -> W02, 2026-05-19 -> W21.
|
|
19
|
+
* 4. telemetryFile(date) composes telemetryDir() + isoWeekFilename(date).
|
|
20
|
+
* 5. emit() writes JSONL with schema_version: 1 (Number), valid
|
|
21
|
+
* ISO-8601 timestamp, session_id from env or 'default', all payload
|
|
22
|
+
* keys present with correct values.
|
|
23
|
+
* 6. emit() throws Error.code='TELEMETRY_VALIDATION' on unknown event.
|
|
24
|
+
* 7. emit() throws on Canon Part 8 forbidden pattern (Cypher in
|
|
25
|
+
* allowed field).
|
|
26
|
+
* 8. emit() creates the v1.13 dir if missing (recursive mkdir); does
|
|
27
|
+
* NOT throw if fs.appendFileSync fails (best-effort -- pipeline
|
|
28
|
+
* must not crash on telemetry disk failure).
|
|
29
|
+
* 9. Two emit() calls 5ms apart land in the SAME ISO-week file.
|
|
30
|
+
* 10. Every line is JSON.parseable (no BOM, single newline terminator).
|
|
31
|
+
*
|
|
32
|
+
* Hermetic: each test creates a tmpdir, points HOME at it, writes, asserts,
|
|
33
|
+
* and tears down via fs.rmSync in finally. Mirrors
|
|
34
|
+
* lib/memory/query-efficiency-telemetry.test.cjs.
|
|
35
|
+
*
|
|
36
|
+
* Registered in lib/memory/run-feynman-tests.cjs.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
const assert = require('node:assert/strict');
|
|
40
|
+
const fs = require('node:fs');
|
|
41
|
+
const path = require('node:path');
|
|
42
|
+
const os = require('node:os');
|
|
43
|
+
|
|
44
|
+
const REPO = path.resolve(__dirname, '..', '..', '..');
|
|
45
|
+
const WRITER_PATH = path.join(REPO, 'lib/core/telemetry/writer.cjs');
|
|
46
|
+
|
|
47
|
+
try { delete require.cache[require.resolve(WRITER_PATH)]; } catch (_) {}
|
|
48
|
+
const writer = require(WRITER_PATH);
|
|
49
|
+
|
|
50
|
+
// ---------- Fixture scaffolding ----------
|
|
51
|
+
|
|
52
|
+
function mkTmpDir(prefix) {
|
|
53
|
+
const base = path.join(os.tmpdir(), 'mos-121-00-' + prefix + '-' + process.pid + '-' + Date.now().toString(36));
|
|
54
|
+
fs.mkdirSync(base, { recursive: true });
|
|
55
|
+
return base;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function rmRf(p) {
|
|
59
|
+
try { fs.rmSync(p, { recursive: true, force: true }); } catch (_) {}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function withHome(tmp, fn) {
|
|
63
|
+
const originalHome = process.env.HOME;
|
|
64
|
+
process.env.HOME = tmp;
|
|
65
|
+
try { return fn(); } finally { process.env.HOME = originalHome; }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readJsonl(filePath) {
|
|
69
|
+
const text = fs.readFileSync(filePath, 'utf8');
|
|
70
|
+
// No BOM allowed, single newline per line.
|
|
71
|
+
assert.ok(text.charCodeAt(0) !== 0xFEFF, 'JSONL must not start with BOM');
|
|
72
|
+
const lines = text.split('\n').filter(l => l.length > 0);
|
|
73
|
+
return lines.map(l => JSON.parse(l));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---------- Test 1: writer export surface ----------
|
|
77
|
+
|
|
78
|
+
(function test1Exports() {
|
|
79
|
+
assert.equal(typeof writer.emit, 'function', 'writer.emit must be a function');
|
|
80
|
+
assert.equal(typeof writer.telemetryDir, 'function', 'writer.telemetryDir must be a function');
|
|
81
|
+
assert.equal(typeof writer.telemetryFile, 'function', 'writer.telemetryFile must be a function');
|
|
82
|
+
assert.equal(typeof writer.isoWeekFilename, 'function', 'writer.isoWeekFilename must be a function');
|
|
83
|
+
assert.ok(Array.isArray(writer.EVENT_TYPES), 'writer must re-export EVENT_TYPES');
|
|
84
|
+
assert.equal(typeof writer.ALLOWED_FIELDS, 'object', 'writer must re-export ALLOWED_FIELDS');
|
|
85
|
+
console.log('PASS test 1: writer exports emit/telemetryDir/telemetryFile/isoWeekFilename/EVENT_TYPES/ALLOWED_FIELDS');
|
|
86
|
+
})();
|
|
87
|
+
|
|
88
|
+
// ---------- Test 2: telemetryDir resolves under HOME ----------
|
|
89
|
+
|
|
90
|
+
(function test2TelemetryDir() {
|
|
91
|
+
const tmp = mkTmpDir('dir');
|
|
92
|
+
try {
|
|
93
|
+
withHome(tmp, () => {
|
|
94
|
+
const dir = writer.telemetryDir();
|
|
95
|
+
const expected = path.join(tmp, '.mindrian', 'telemetry', 'v1.13');
|
|
96
|
+
assert.equal(dir, expected,
|
|
97
|
+
'telemetryDir() must resolve to <HOME>/.mindrian/telemetry/v1.13; got ' + dir);
|
|
98
|
+
});
|
|
99
|
+
console.log('PASS test 2: telemetryDir() resolves under HOME');
|
|
100
|
+
} finally {
|
|
101
|
+
rmRf(tmp);
|
|
102
|
+
}
|
|
103
|
+
})();
|
|
104
|
+
|
|
105
|
+
// ---------- Test 3: isoWeekFilename zero-pads + handles year boundaries ----------
|
|
106
|
+
|
|
107
|
+
(function test3IsoWeek() {
|
|
108
|
+
// 2026-05-19 is a Tuesday in ISO week 21 of 2026.
|
|
109
|
+
const f1 = writer.isoWeekFilename(new Date('2026-05-19T12:00:00Z'));
|
|
110
|
+
assert.equal(f1, 'events-2026-W21.jsonl',
|
|
111
|
+
'isoWeekFilename(2026-05-19) must be events-2026-W21.jsonl; got ' + f1);
|
|
112
|
+
|
|
113
|
+
// 2026-01-05 is a Monday in ISO week 02 of 2026 (W01 is the week of
|
|
114
|
+
// 2025-12-29..2026-01-04 because the first Thursday of 2026 is 2026-01-01).
|
|
115
|
+
const f2 = writer.isoWeekFilename(new Date('2026-01-05T12:00:00Z'));
|
|
116
|
+
assert.equal(f2, 'events-2026-W02.jsonl',
|
|
117
|
+
'isoWeekFilename(2026-01-05) must be events-2026-W02.jsonl (zero-padded); got ' + f2);
|
|
118
|
+
|
|
119
|
+
// 2026-01-01 is the Thursday that anchors ISO week 01 of 2026.
|
|
120
|
+
const f3 = writer.isoWeekFilename(new Date('2026-01-01T12:00:00Z'));
|
|
121
|
+
assert.equal(f3, 'events-2026-W01.jsonl',
|
|
122
|
+
'isoWeekFilename(2026-01-01) must be events-2026-W01.jsonl; got ' + f3);
|
|
123
|
+
|
|
124
|
+
console.log('PASS test 3: isoWeekFilename zero-pads week (W21, W02, W01)');
|
|
125
|
+
})();
|
|
126
|
+
|
|
127
|
+
// ---------- Test 4: telemetryFile composes ----------
|
|
128
|
+
|
|
129
|
+
(function test4TelemetryFile() {
|
|
130
|
+
const tmp = mkTmpDir('file');
|
|
131
|
+
try {
|
|
132
|
+
withHome(tmp, () => {
|
|
133
|
+
const f = writer.telemetryFile(new Date('2026-05-19T00:00:00Z'));
|
|
134
|
+
const expected = path.join(tmp, '.mindrian', 'telemetry', 'v1.13', 'events-2026-W21.jsonl');
|
|
135
|
+
assert.equal(f, expected,
|
|
136
|
+
'telemetryFile() must compose dir + isoWeekFilename; got ' + f);
|
|
137
|
+
});
|
|
138
|
+
console.log('PASS test 4: telemetryFile() composes dir + filename');
|
|
139
|
+
} finally {
|
|
140
|
+
rmRf(tmp);
|
|
141
|
+
}
|
|
142
|
+
})();
|
|
143
|
+
|
|
144
|
+
// ---------- Test 5: emit() writes JSONL with schema_version + iso timestamp + session_id + payload ----------
|
|
145
|
+
|
|
146
|
+
(function test5EmitWritesJsonl() {
|
|
147
|
+
const tmp = mkTmpDir('emit');
|
|
148
|
+
const originalSession = process.env.CLAUDE_SESSION_ID;
|
|
149
|
+
process.env.CLAUDE_SESSION_ID = 'test-session-abc';
|
|
150
|
+
try {
|
|
151
|
+
withHome(tmp, () => {
|
|
152
|
+
writer.emit('selector_pick', {
|
|
153
|
+
sub_shape: 'F.1',
|
|
154
|
+
mode: 'A',
|
|
155
|
+
ranker_confidence: 0.73,
|
|
156
|
+
recommended_rendered: true,
|
|
157
|
+
options_count: 4,
|
|
158
|
+
room_slug_sha256: 'a'.repeat(64),
|
|
159
|
+
verb_chosen: 'Run Methodology',
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const filePath = writer.telemetryFile();
|
|
163
|
+
assert.ok(fs.existsSync(filePath), 'emit() must create the JSONL file at ' + filePath);
|
|
164
|
+
|
|
165
|
+
const rows = readJsonl(filePath);
|
|
166
|
+
assert.equal(rows.length, 1, 'emit() must write exactly 1 line; got ' + rows.length);
|
|
167
|
+
|
|
168
|
+
const r = rows[0];
|
|
169
|
+
assert.equal(r.event, 'selector_pick', 'event field must be selector_pick');
|
|
170
|
+
assert.equal(r.schema_version, 1, 'schema_version must be Number 1; got ' + r.schema_version + ' (' + typeof r.schema_version + ')');
|
|
171
|
+
assert.equal(typeof r.schema_version, 'number', 'schema_version must be typeof number');
|
|
172
|
+
assert.match(r.timestamp, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/, 'timestamp must be ISO-8601');
|
|
173
|
+
assert.equal(typeof r.session_id, 'string', 'session_id must be a string');
|
|
174
|
+
assert.ok(r.session_id.length > 0, 'session_id must be non-empty');
|
|
175
|
+
assert.equal(r.session_id, 'test-session-abc', 'session_id must reflect CLAUDE_SESSION_ID env');
|
|
176
|
+
|
|
177
|
+
// All 7 payload keys present with correct values.
|
|
178
|
+
assert.equal(r.sub_shape, 'F.1');
|
|
179
|
+
assert.equal(r.mode, 'A');
|
|
180
|
+
assert.equal(r.ranker_confidence, 0.73);
|
|
181
|
+
assert.equal(r.recommended_rendered, true);
|
|
182
|
+
assert.equal(r.options_count, 4);
|
|
183
|
+
assert.equal(r.room_slug_sha256, 'a'.repeat(64));
|
|
184
|
+
assert.equal(r.verb_chosen, 'Run Methodology');
|
|
185
|
+
});
|
|
186
|
+
console.log('PASS test 5: emit() writes JSONL with schema_version=1, ISO timestamp, session_id, all 7 payload keys');
|
|
187
|
+
} finally {
|
|
188
|
+
if (originalSession === undefined) { delete process.env.CLAUDE_SESSION_ID; }
|
|
189
|
+
else { process.env.CLAUDE_SESSION_ID = originalSession; }
|
|
190
|
+
rmRf(tmp);
|
|
191
|
+
}
|
|
192
|
+
})();
|
|
193
|
+
|
|
194
|
+
// ---------- Test 6: emit() throws on unknown event ----------
|
|
195
|
+
|
|
196
|
+
(function test6UnknownEventThrows() {
|
|
197
|
+
const tmp = mkTmpDir('unknown');
|
|
198
|
+
try {
|
|
199
|
+
withHome(tmp, () => {
|
|
200
|
+
assert.throws(() => writer.emit('definitely_not_a_real_event', {}),
|
|
201
|
+
(err) => err.code === 'TELEMETRY_VALIDATION',
|
|
202
|
+
'emit() on unknown event must throw with code TELEMETRY_VALIDATION');
|
|
203
|
+
});
|
|
204
|
+
console.log('PASS test 6: emit() throws TELEMETRY_VALIDATION on unknown event');
|
|
205
|
+
} finally {
|
|
206
|
+
rmRf(tmp);
|
|
207
|
+
}
|
|
208
|
+
})();
|
|
209
|
+
|
|
210
|
+
// ---------- Test 7: emit() throws on Canon Part 8 forbidden pattern ----------
|
|
211
|
+
|
|
212
|
+
(function test7ForbiddenPatternThrows() {
|
|
213
|
+
const tmp = mkTmpDir('forbidden');
|
|
214
|
+
try {
|
|
215
|
+
withHome(tmp, () => {
|
|
216
|
+
assert.throws(() => writer.emit('selector_pick', {
|
|
217
|
+
sub_shape: 'F.1',
|
|
218
|
+
verb_chosen: 'MATCH (n:Framework) RETURN n', // Cypher injection
|
|
219
|
+
}), (err) => err.code === 'TELEMETRY_VALIDATION',
|
|
220
|
+
'emit() must throw on Cypher fragment in allowed field');
|
|
221
|
+
});
|
|
222
|
+
console.log('PASS test 7: emit() throws on Canon Part 8 forbidden pattern (Cypher)');
|
|
223
|
+
} finally {
|
|
224
|
+
rmRf(tmp);
|
|
225
|
+
}
|
|
226
|
+
})();
|
|
227
|
+
|
|
228
|
+
// ---------- Test 8: emit() creates dir + swallows fs errors silently ----------
|
|
229
|
+
|
|
230
|
+
(function test8DirCreationAndFsErrorSilent() {
|
|
231
|
+
// Step A: emit into a HOME that does NOT yet contain .mindrian/telemetry/v1.13.
|
|
232
|
+
const tmp = mkTmpDir('create');
|
|
233
|
+
try {
|
|
234
|
+
withHome(tmp, () => {
|
|
235
|
+
// Verify the dir does not exist yet.
|
|
236
|
+
const dir = writer.telemetryDir();
|
|
237
|
+
assert.equal(fs.existsSync(dir), false, 'dir must NOT exist pre-emit');
|
|
238
|
+
writer.emit('selector_pick', {
|
|
239
|
+
sub_shape: 'F.1',
|
|
240
|
+
mode: 'A',
|
|
241
|
+
options_count: 1,
|
|
242
|
+
});
|
|
243
|
+
assert.equal(fs.existsSync(dir), true, 'emit() must create the v1.13 dir recursively');
|
|
244
|
+
const filePath = writer.telemetryFile();
|
|
245
|
+
assert.equal(fs.existsSync(filePath), true, 'emit() must create the JSONL file');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Step B: emit when fs.appendFileSync is monkeypatched to throw. Pipeline
|
|
249
|
+
// must not crash. Done in a fresh subprocess so we can monkeypatch without
|
|
250
|
+
// breaking the rest of the suite.
|
|
251
|
+
const tmp2 = mkTmpDir('fserr');
|
|
252
|
+
withHome(tmp2, () => {
|
|
253
|
+
const realAppend = fs.appendFileSync;
|
|
254
|
+
fs.appendFileSync = function () { throw new Error('disk full simulated'); };
|
|
255
|
+
try {
|
|
256
|
+
writer.emit('selector_pick', { sub_shape: 'F.1', mode: 'A' });
|
|
257
|
+
// If we reach here, the swallow worked.
|
|
258
|
+
} finally {
|
|
259
|
+
fs.appendFileSync = realAppend;
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
rmRf(tmp2);
|
|
263
|
+
|
|
264
|
+
console.log('PASS test 8: emit() creates dir recursively + swallows fs errors silently');
|
|
265
|
+
} finally {
|
|
266
|
+
rmRf(tmp);
|
|
267
|
+
}
|
|
268
|
+
})();
|
|
269
|
+
|
|
270
|
+
// ---------- Test 9: two emits 5ms apart land in the same ISO-week file ----------
|
|
271
|
+
|
|
272
|
+
(function test9SameFileAcrossShortInterval() {
|
|
273
|
+
const tmp = mkTmpDir('sameweek');
|
|
274
|
+
try {
|
|
275
|
+
withHome(tmp, () => {
|
|
276
|
+
writer.emit('selector_pick', { sub_shape: 'F.1', mode: 'A' });
|
|
277
|
+
const sleep5 = Date.now() + 5;
|
|
278
|
+
while (Date.now() < sleep5) {} // busy-wait
|
|
279
|
+
writer.emit('selector_pick', { sub_shape: 'F.2', mode: 'B' });
|
|
280
|
+
|
|
281
|
+
const filePath = writer.telemetryFile();
|
|
282
|
+
const rows = readJsonl(filePath);
|
|
283
|
+
assert.equal(rows.length, 2,
|
|
284
|
+
'two emits 5ms apart must land in same file -> 2 lines; got ' + rows.length);
|
|
285
|
+
assert.equal(rows[0].sub_shape, 'F.1');
|
|
286
|
+
assert.equal(rows[1].sub_shape, 'F.2');
|
|
287
|
+
});
|
|
288
|
+
console.log('PASS test 9: two emits 5ms apart in same ISO-week file');
|
|
289
|
+
} finally {
|
|
290
|
+
rmRf(tmp);
|
|
291
|
+
}
|
|
292
|
+
})();
|
|
293
|
+
|
|
294
|
+
// ---------- Test 10: lines are JSON.parseable, no BOM, no trailing whitespace ----------
|
|
295
|
+
|
|
296
|
+
(function test10ParseableLines() {
|
|
297
|
+
const tmp = mkTmpDir('parse');
|
|
298
|
+
try {
|
|
299
|
+
withHome(tmp, () => {
|
|
300
|
+
writer.emit('selector_pick', { sub_shape: 'F.1', mode: 'A' });
|
|
301
|
+
writer.emit('command_invocation', { command: '/mos:status', outcome: 'success', duration_ms: 42 });
|
|
302
|
+
writer.emit('nav_bypass', { op: 'read', reason: 'legacy_path' });
|
|
303
|
+
|
|
304
|
+
const filePath = writer.telemetryFile();
|
|
305
|
+
const text = fs.readFileSync(filePath, 'utf8');
|
|
306
|
+
|
|
307
|
+
// BOM check.
|
|
308
|
+
assert.ok(text.charCodeAt(0) !== 0xFEFF, 'no BOM at start');
|
|
309
|
+
|
|
310
|
+
// Each line must JSON.parse.
|
|
311
|
+
const lines = text.split('\n');
|
|
312
|
+
// Last element after split('\n') is '' if file ends with \n (which it should).
|
|
313
|
+
assert.equal(lines[lines.length - 1], '', 'file must end with newline (last split chunk is empty string)');
|
|
314
|
+
|
|
315
|
+
const dataLines = lines.slice(0, -1);
|
|
316
|
+
assert.equal(dataLines.length, 3, 'must have 3 data lines; got ' + dataLines.length);
|
|
317
|
+
for (const line of dataLines) {
|
|
318
|
+
assert.equal(line[line.length - 1] !== ' ' && line[line.length - 1] !== '\r', true,
|
|
319
|
+
'no trailing whitespace on line: ' + JSON.stringify(line));
|
|
320
|
+
const obj = JSON.parse(line); // throws if malformed
|
|
321
|
+
assert.equal(typeof obj.event, 'string');
|
|
322
|
+
assert.equal(obj.schema_version, 1);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
console.log('PASS test 10: every line is JSON.parseable, no BOM, single newline terminator');
|
|
326
|
+
} finally {
|
|
327
|
+
rmRf(tmp);
|
|
328
|
+
}
|
|
329
|
+
})();
|
|
330
|
+
|
|
331
|
+
console.log('\nwriter.test.cjs: 10/10 tests passed');
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
5
|
+
*
|
|
6
|
+
* Phase 121.5-07 Task 1 -- Terminal capability probe.
|
|
7
|
+
*
|
|
8
|
+
* Decision D-05 (LOCKED):
|
|
9
|
+
* probeCapability(opts) returns one of:
|
|
10
|
+
* 'truecolor' -- process.stdout.isTTY === true AND ($COLORTERM matches
|
|
11
|
+
* truecolor/24bit OR equals literal "truecolor")
|
|
12
|
+
* '256color' -- isTTY === true AND ($COLORTERM contains "256color" OR
|
|
13
|
+
* $TERM contains "256color")
|
|
14
|
+
* 'ascii' -- otherwise (non-TTY, no COLORTERM, Desktop chat surface)
|
|
15
|
+
*
|
|
16
|
+
* Desktop hard-override: env.CLAUDE_DESKTOP=1 or env.MINDRIAN_DESKTOP=1 forces
|
|
17
|
+
* 'ascii' regardless of TTY state. This is the Desktop chat surface, which
|
|
18
|
+
* strips ANSI escapes.
|
|
19
|
+
*
|
|
20
|
+
* Canon references:
|
|
21
|
+
* Part 3 UI Ruling System -- bulletproof terminal coherence depends on a
|
|
22
|
+
* single capability probe shared across surfaces.
|
|
23
|
+
* Part 7 Reuse Before Build -- this is the REUSABLE substrate for
|
|
24
|
+
* /mos:status, /mos:doctor, /mos:splash, /mos:help renderer.
|
|
25
|
+
* Part 8 Graph Boundary -- reads env-vars + isTTY only; zero network,
|
|
26
|
+
* zero Brain, zero telemetry egress.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Probe the current terminal capability.
|
|
31
|
+
*
|
|
32
|
+
* @param {object} [opts]
|
|
33
|
+
* @param {object} [opts.env] -- env override (defaults to process.env)
|
|
34
|
+
* @param {boolean} [opts.isTTY] -- TTY override (defaults to process.stdout.isTTY)
|
|
35
|
+
* @returns {'truecolor'|'256color'|'ascii'}
|
|
36
|
+
*/
|
|
37
|
+
function probeCapability(opts) {
|
|
38
|
+
const o = opts || {};
|
|
39
|
+
const env = o.env || process.env;
|
|
40
|
+
const isTTY =
|
|
41
|
+
o.isTTY !== undefined
|
|
42
|
+
? o.isTTY
|
|
43
|
+
: Boolean(process.stdout && process.stdout.isTTY);
|
|
44
|
+
|
|
45
|
+
// Desktop simulator hard-override.
|
|
46
|
+
if (env.CLAUDE_DESKTOP === '1' || env.MINDRIAN_DESKTOP === '1') {
|
|
47
|
+
return 'ascii';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Test/CI hard-override for harnesses that pipe stdout (isTTY false).
|
|
51
|
+
// Used by the Phase 121.5-09 coherence smoke test harness to verify the
|
|
52
|
+
// truecolor branch fires on the CLI surface. Production code paths never
|
|
53
|
+
// set this; isTTY-based detection remains the default contract.
|
|
54
|
+
if (env.MINDRIAN_FORCE_TRUECOLOR === '1') return 'truecolor';
|
|
55
|
+
if (env.MINDRIAN_FORCE_256COLOR === '1') return '256color';
|
|
56
|
+
|
|
57
|
+
if (!isTTY) return 'ascii';
|
|
58
|
+
|
|
59
|
+
const colorterm = String(env.COLORTERM || '').toLowerCase();
|
|
60
|
+
const term = String(env.TERM || '').toLowerCase();
|
|
61
|
+
|
|
62
|
+
if (colorterm === 'truecolor' || /truecolor/.test(colorterm) || /24bit/.test(colorterm)) {
|
|
63
|
+
return 'truecolor';
|
|
64
|
+
}
|
|
65
|
+
if (colorterm.includes('256color') || term.includes('256color')) {
|
|
66
|
+
return '256color';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return 'ascii';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {'truecolor'|'256color'|'ascii'} capability
|
|
74
|
+
* @returns {boolean}
|
|
75
|
+
*/
|
|
76
|
+
function supportsColor(capability) {
|
|
77
|
+
return capability === 'truecolor' || capability === '256color';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {'truecolor'|'256color'|'ascii'} capability
|
|
82
|
+
* @returns {boolean}
|
|
83
|
+
*/
|
|
84
|
+
function supportsTruecolor(capability) {
|
|
85
|
+
return capability === 'truecolor';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = { probeCapability, supportsColor, supportsTruecolor };
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
5
|
+
*
|
|
6
|
+
* Phase 119-00 Wave 1 -- Venture-Shaped-Turn Nudge (D-02 detector).
|
|
7
|
+
*
|
|
8
|
+
* shouldSurfaceNudge(roomDir, opts) -> {surface, turn_count, threshold, skip_reason?}
|
|
9
|
+
*
|
|
10
|
+
* D-02 invariant: after N=3 consecutive venture-shaped conversational turns
|
|
11
|
+
* with no upload AND no active room, surface an F.1 selector offering
|
|
12
|
+
* [upload material] / [/mos:new-project] / [keep talking]. This is the ONLY
|
|
13
|
+
* auto-create-adjacent path for prompt-only sessions (D-01 prohibits
|
|
14
|
+
* auto-creating from prompts alone).
|
|
15
|
+
*
|
|
16
|
+
* D-01 invariant: the upload path takes precedence. If any
|
|
17
|
+
* 'auto_explore_fired' event appears in the recent window, the nudge does
|
|
18
|
+
* NOT surface -- the Phase 117 detector is already handling first-material.
|
|
19
|
+
*
|
|
20
|
+
* Canon Part 8 boundary -- user_text classification:
|
|
21
|
+
* The user-conversational "venture-shaped" classification CANNOT run on
|
|
22
|
+
* raw user text inside this module. The event-log schema (per Canon Part
|
|
23
|
+
* 8 fence + Phase 90-06 cross-room aggregator) structurally excludes
|
|
24
|
+
* user_text -- the memory_event log is queried by cross-room aggregators
|
|
25
|
+
* and storing raw user content there would breach the boundary.
|
|
26
|
+
*
|
|
27
|
+
* Alternative signal contract: the upstream telemetry surface (the
|
|
28
|
+
* PostToolUse / UserPromptSubmit hook OR the Phase 115 dual-path-detector
|
|
29
|
+
* seam) classifies sentences at WRITE TIME via
|
|
30
|
+
* lib/core/mva-classifier.cjs::classify and stores ONLY a scalar boolean
|
|
31
|
+
* `venture_classified` + classification_source enum in
|
|
32
|
+
* f_selector_decision.properties. The nudge module reads the scalar at
|
|
33
|
+
* decision time.
|
|
34
|
+
*
|
|
35
|
+
* v1.13.0 safe-default: if `properties.venture_classified` is structurally
|
|
36
|
+
* absent at execution time, the nudge degrades to surface:false with
|
|
37
|
+
* skip_reason:'venture_classification_unavailable'. A v1.14.0 phase wires
|
|
38
|
+
* the upstream classification at the f_selector_decision emission site.
|
|
39
|
+
*
|
|
40
|
+
* Canon Part 9 chokepoint:
|
|
41
|
+
* - All reads route through lib/core/navigation.cjs::findRecentChanges.
|
|
42
|
+
* - room.db is opened via node:sqlite DatabaseSync directly (mirrors the
|
|
43
|
+
* Phase 117 scripts/auto-explore-fingerprint.cjs pattern; node:sqlite
|
|
44
|
+
* usage from a CLI-adjacent module is not a chokepoint violation per
|
|
45
|
+
* the Phase 109-06 pre-commit hook which gates only direct require of
|
|
46
|
+
* room-db.cjs).
|
|
47
|
+
*
|
|
48
|
+
* Pure CJS, node built-ins + lib/core/navigation. NO require of room-db.cjs.
|
|
49
|
+
* NO require of dual-path-detector.cjs at read-time (the upstream
|
|
50
|
+
* classification has already happened at write-time per the alternative
|
|
51
|
+
* signal contract above). NO require of brain-client. Canon Part 8 preserved.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
const path = require('node:path');
|
|
55
|
+
const fs = require('node:fs');
|
|
56
|
+
|
|
57
|
+
const VENTURE_NUDGE_THRESHOLD = 3;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* shouldSurfaceNudge(roomDir, opts) -> { surface, turn_count, threshold, skip_reason? }
|
|
61
|
+
*
|
|
62
|
+
* @param {string} roomDir - absolute path to a room directory.
|
|
63
|
+
* @param {object} [opts] - reserved for future extension (D-02 threshold override).
|
|
64
|
+
* @returns {object}
|
|
65
|
+
*/
|
|
66
|
+
function shouldSurfaceNudge(roomDir, opts) {
|
|
67
|
+
const options = opts || {};
|
|
68
|
+
const threshold = Number.isInteger(options.threshold) && options.threshold > 0
|
|
69
|
+
? options.threshold
|
|
70
|
+
: VENTURE_NUDGE_THRESHOLD;
|
|
71
|
+
|
|
72
|
+
// Guard 1: no roomDir -> safe default.
|
|
73
|
+
if (!roomDir || typeof roomDir !== 'string') {
|
|
74
|
+
return { surface: false, turn_count: 0, threshold: threshold, skip_reason: 'no_room_dir' };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Guard 2: room.db missing -> cold-start safe default. No false positives.
|
|
78
|
+
const dbPath = path.join(roomDir, '.mindrian', 'room.db');
|
|
79
|
+
if (!fs.existsSync(dbPath)) {
|
|
80
|
+
return { surface: false, turn_count: 0, threshold: threshold, skip_reason: 'no_room_db' };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Open the db read-only via node:sqlite DatabaseSync. Mirrors the Phase 117
|
|
84
|
+
// scripts/auto-explore-fingerprint.cjs::dailyCapHit() pattern. Lazy-require
|
|
85
|
+
// so environments lacking node:sqlite degrade to surface:false.
|
|
86
|
+
let DatabaseSync;
|
|
87
|
+
try {
|
|
88
|
+
({ DatabaseSync } = require('node:sqlite'));
|
|
89
|
+
} catch (_e) {
|
|
90
|
+
return { surface: false, turn_count: 0, threshold: threshold, skip_reason: 'no_sqlite' };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let db;
|
|
94
|
+
try {
|
|
95
|
+
db = new DatabaseSync(dbPath);
|
|
96
|
+
} catch (_e) {
|
|
97
|
+
return { surface: false, turn_count: 0, threshold: threshold, skip_reason: 'db_open_failed' };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let recent;
|
|
101
|
+
try {
|
|
102
|
+
const navigation = require('./navigation.cjs');
|
|
103
|
+
// Window = last 24 hours; cap at 200 events for safety.
|
|
104
|
+
const sinceMs = Date.now() - (24 * 60 * 60 * 1000);
|
|
105
|
+
recent = navigation.findRecentChanges(db, sinceMs, { limit: 200 });
|
|
106
|
+
} catch (_e) {
|
|
107
|
+
try { db.close(); } catch (_ignore) { /* graceful */ }
|
|
108
|
+
return { surface: false, turn_count: 0, threshold: threshold, skip_reason: 'navigation_query_failed' };
|
|
109
|
+
} finally {
|
|
110
|
+
try { db.close(); } catch (_ignore) { /* graceful */ }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!Array.isArray(recent)) {
|
|
114
|
+
return { surface: false, turn_count: 0, threshold: threshold, skip_reason: 'empty_window' };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// D-01 invariant short-circuit: upload path takes precedence.
|
|
118
|
+
for (const event of recent) {
|
|
119
|
+
if (event && event.eventType === 'auto_explore_fired') {
|
|
120
|
+
return { surface: false, turn_count: 0, threshold: threshold, skip_reason: 'upload_path_active' };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Filter to user-conversational-turn signal events. For v1.13.0-beta.18
|
|
125
|
+
// scope, "conversational turn" is approximated by f_selector_decision
|
|
126
|
+
// (the event that fires on every meaningful F-shape selection per Phase
|
|
127
|
+
// 125-06). focus_changed is intentionally excluded -- it is a navigation
|
|
128
|
+
// event, not a conversation turn. Precise per-turn detection is deferred
|
|
129
|
+
// to Phase 121 trajectory telemetry.
|
|
130
|
+
//
|
|
131
|
+
// Per Canon Part 8 alternative signal contract: count only events where
|
|
132
|
+
// properties.venture_classified === true. If the field is structurally
|
|
133
|
+
// absent (NO event in the window carries it), degrade to safe-default
|
|
134
|
+
// skip_reason:'venture_classification_unavailable' -- the v1.14.0
|
|
135
|
+
// upstream-classification surface has not yet wired the scalar.
|
|
136
|
+
let turnCount = 0;
|
|
137
|
+
let sawClassifiedField = false;
|
|
138
|
+
for (const event of recent) {
|
|
139
|
+
if (!event || event.eventType !== 'f_selector_decision') continue;
|
|
140
|
+
const props = event.properties || {};
|
|
141
|
+
if (typeof props.venture_classified === 'boolean') {
|
|
142
|
+
sawClassifiedField = true;
|
|
143
|
+
if (props.venture_classified === true) turnCount += 1;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!sawClassifiedField) {
|
|
148
|
+
return {
|
|
149
|
+
surface: false,
|
|
150
|
+
turn_count: 0,
|
|
151
|
+
threshold: threshold,
|
|
152
|
+
skip_reason: 'venture_classification_unavailable',
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const surface = turnCount >= threshold;
|
|
157
|
+
return { surface: surface, turn_count: turnCount, threshold: threshold };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = {
|
|
161
|
+
shouldSurfaceNudge: shouldSurfaceNudge,
|
|
162
|
+
VENTURE_NUDGE_THRESHOLD: VENTURE_NUDGE_THRESHOLD,
|
|
163
|
+
};
|