polymath-society 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +52 -0
- package/README.md +311 -0
- package/dist/DATA-MAP.md +109 -0
- package/dist/cli.js +24136 -0
- package/dist/engine/closing-rubric.md +50 -0
- package/dist/engine/exp-allfacets.js +16832 -0
- package/dist/engine/exp-person.js +16922 -0
- package/dist/engine/exp-pipeline.js +16700 -0
- package/dist/engine/feel-seen-rubric.md +192 -0
- package/dist/engine/ingest-export.js +1423 -0
- package/dist/engine/peak-demos.js +16752 -0
- package/dist/engine/person-dimension-summary.js +16744 -0
- package/dist/engine/person-facet-lines.js +16784 -0
- package/dist/engine/person-headline.js +16733 -0
- package/dist/engine/person-report.js +16845 -0
- package/dist/engine/person-self-image.js +16689 -0
- package/dist/engine/public-report-guidelines.md +80 -0
- package/dist/engine/public-report.js +17284 -0
- package/dist/engine/run-analysis.js +16035 -0
- package/dist/engine/run-tagger.js +16092 -0
- package/dist/engine/top-companies.md +41 -0
- package/dist/engine/writing-well.md +56 -0
- package/dist/index.js +22021 -0
- package/dist/pipeline/TECHNIQUES.md +406 -0
- package/dist/pipeline/WRITING.md +48 -0
- package/dist/pipeline/coding-agglomerate.js +15876 -0
- package/dist/pipeline/coding-aggregate.js +440 -0
- package/dist/pipeline/coding-build.js +1168 -0
- package/dist/pipeline/coding-coaching.js +16893 -0
- package/dist/pipeline/coding-day-digest.js +15869 -0
- package/dist/pipeline/coding-delegation.js +16292 -0
- package/dist/pipeline/coding-expertise.js +15925 -0
- package/dist/pipeline/coding-flow.js +313 -0
- package/dist/pipeline/coding-frontier-detail.js +15878 -0
- package/dist/pipeline/coding-frontier.js +51 -0
- package/dist/pipeline/coding-gap-dist.js +293 -0
- package/dist/pipeline/coding-gap.js +17108 -0
- package/dist/pipeline/coding-grade.js +16195 -0
- package/dist/pipeline/coding-nutshell.js +15725 -0
- package/dist/pipeline/coding-projects.js +104 -0
- package/dist/pipeline/coding-walkthrough.js +15947 -0
- package/dist/web/app.js +12024 -0
- package/dist/web/index.html +1 -0
- package/dist/web/styles.css +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import{createRequire as __cr}from'module';const require=__cr(import.meta.url);
|
|
2
|
+
|
|
3
|
+
// ../../scripts/coding-frontier.mts
|
|
4
|
+
import { promises as fs } from "fs";
|
|
5
|
+
var CODING = ".data/coding";
|
|
6
|
+
var tc = (s, name) => (s.toolCounts?.[name] ?? 0) > 0;
|
|
7
|
+
var mcp = (s, re) => (s.mcpTools ?? []).some((m) => re.test(m));
|
|
8
|
+
var DETECTORS = [
|
|
9
|
+
{ key: "live-preview", label: "Live-preview automation (drives a dev server to verify UI)", family: "Browser & machine control", test: (s) => mcp(s, /Claude_Preview|preview_/) },
|
|
10
|
+
{ key: "chrome-mcp", label: "Chrome / browser automation", family: "Browser & machine control", test: (s) => mcp(s, /Claude_in_Chrome|playwright/) },
|
|
11
|
+
{ key: "computer-use", label: "Computer use (native desktop control)", family: "Browser & machine control", test: (s) => mcp(s, /computer-use/) },
|
|
12
|
+
{ key: "subagent-fanout", label: "Subagent fan-out (parallel agents)", family: "Orchestration", test: (s) => (s.subagentSpawns ?? 0) > 0 || tc(s, "Agent") || tc(s, "Task") },
|
|
13
|
+
{ key: "task-orchestration", label: "Task orchestration (TaskCreate / TaskUpdate)", family: "Orchestration", test: (s) => tc(s, "TaskCreate") || tc(s, "TaskUpdate") },
|
|
14
|
+
{ key: "deferred-tools", label: "Deferred-tool loading (ToolSearch)", family: "Orchestration", test: (s) => tc(s, "ToolSearch") },
|
|
15
|
+
{ key: "queue-ahead", label: "Queue-ahead (prompts queued while the AI works)", family: "Orchestration", test: (s) => (s.queueOps ?? 0) > 0 },
|
|
16
|
+
{ key: "skill-used", label: "Used a skill", family: "Skills & extensions", test: (s) => tc(s, "Skill") },
|
|
17
|
+
{ key: "visualize", label: "Visualization widgets", family: "Skills & extensions", test: (s) => mcp(s, /visualize|show_widget/) },
|
|
18
|
+
{ key: "web-research", label: "Web research (WebSearch / WebFetch)", family: "Research", test: (s) => tc(s, "WebSearch") || tc(s, "WebFetch") }
|
|
19
|
+
];
|
|
20
|
+
async function main() {
|
|
21
|
+
const all = JSON.parse(await fs.readFile(`${CODING}/sessions.json`, "utf8"));
|
|
22
|
+
const sessions = all.filter((s) => s.klass === "interactive");
|
|
23
|
+
const dateOf = (s) => (s.start || "").slice(0, 10);
|
|
24
|
+
const activeDays = new Set(sessions.map(dateOf).filter(Boolean)).size;
|
|
25
|
+
const capabilities = DETECTORS.map((d) => {
|
|
26
|
+
const hit = sessions.filter((s) => d.test(s));
|
|
27
|
+
const days = new Set(hit.map(dateOf).filter(Boolean)).size;
|
|
28
|
+
return { key: d.key, label: d.label, family: d.family, sessions: hit.length, days, tableStakes: hit.length / sessions.length >= 0.85 };
|
|
29
|
+
}).filter((c) => c.sessions > 0).sort((a, b) => b.sessions - a.sessions);
|
|
30
|
+
const srv = {};
|
|
31
|
+
for (const s of sessions) for (const m of s.mcpTools ?? []) {
|
|
32
|
+
const mm = /^mcp__(.+?)__/.exec(m);
|
|
33
|
+
if (mm) (srv[mm[1]] = srv[mm[1]] || /* @__PURE__ */ new Set()).add(s.sessionId);
|
|
34
|
+
}
|
|
35
|
+
const mcpServers = Object.entries(srv).map(([server, set]) => ({ server, sessions: set.size })).sort((a, b) => b.sessions - a.sessions);
|
|
36
|
+
const out = {
|
|
37
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
38
|
+
sessionsScanned: sessions.length,
|
|
39
|
+
activeDays,
|
|
40
|
+
capabilities,
|
|
41
|
+
mcpServers,
|
|
42
|
+
pending: ["skill-created (needs tool-input parse)", "loops / scheduled runs", "overnight / EC2 remote", "run-from-phone"]
|
|
43
|
+
};
|
|
44
|
+
await fs.writeFile(`${CODING}/frontier.json`, JSON.stringify(out, null, 2));
|
|
45
|
+
console.log(`frontier.json \u2014 ${capabilities.length} capabilities, ${mcpServers.length} MCP servers, ${sessions.length} sessions`);
|
|
46
|
+
capabilities.forEach((c) => console.log(` ${c.sessions}\xD7 (${c.days}d) ${c.label}${c.tableStakes ? " [table-stakes]" : ""}`));
|
|
47
|
+
}
|
|
48
|
+
main().catch((e) => {
|
|
49
|
+
console.error(e);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
});
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import{createRequire as __cr}from'module';const require=__cr(import.meta.url);
|
|
2
|
+
|
|
3
|
+
// ../../scripts/coding-gap-dist.mts
|
|
4
|
+
import { promises as fs2 } from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
|
|
7
|
+
// ../../lib/agents/shared/cliAdapter.ts
|
|
8
|
+
var CLAUDE_BIN = process.env.CLAUDE_BIN || "claude";
|
|
9
|
+
|
|
10
|
+
// ../../lib/agents/shared/backends.ts
|
|
11
|
+
import os from "os";
|
|
12
|
+
var HOME = os.homedir();
|
|
13
|
+
|
|
14
|
+
// ../../lib/agents/shared/tools.ts
|
|
15
|
+
import { execFile } from "child_process";
|
|
16
|
+
import { promisify } from "util";
|
|
17
|
+
var exec = promisify(execFile);
|
|
18
|
+
|
|
19
|
+
// ../../lib/agents/shared/limitGate.ts
|
|
20
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
21
|
+
var MIN = 6e4;
|
|
22
|
+
var HOUR = 60 * MIN;
|
|
23
|
+
var STALL_BACKOFFS_MS = [6e4, 3 * MIN, 10 * MIN, 20 * MIN];
|
|
24
|
+
var SLEEP_CHUNK_MS = 5 * MIN;
|
|
25
|
+
var CANONICAL = [
|
|
26
|
+
/claude (ai )?usage limit reached/i,
|
|
27
|
+
/\busage limit reached\b/i,
|
|
28
|
+
// "session" variant seen live 2026-07-06: "You've hit your session limit ·
|
|
29
|
+
// resets 4:10am (America/Los_Angeles)" — unrecognized, it failed EVERY
|
|
30
|
+
// post-wall stage of the overnight rehearsal instead of pausing. Keep the
|
|
31
|
+
// subject alternatives in sync with what the CLI actually emits.
|
|
32
|
+
/you'?ve (?:hit|reached) your (?:usage |session |weekly |daily )?limit/i,
|
|
33
|
+
/\bsession limit\b[^.]{0,40}resets?/i,
|
|
34
|
+
/reached your usage limit/i,
|
|
35
|
+
/5[\s-]?hour limit (?:reached|hit)/i,
|
|
36
|
+
/limit will reset/i
|
|
37
|
+
];
|
|
38
|
+
var LIBERAL = [
|
|
39
|
+
...CANONICAL,
|
|
40
|
+
/usage limit/i,
|
|
41
|
+
/\bsession limit\b/i,
|
|
42
|
+
/\brate[\s-]?limit(?:ed|s|\b)/i,
|
|
43
|
+
/\btoo many requests\b/i,
|
|
44
|
+
/\b429\b/,
|
|
45
|
+
/quota (?:exceeded|reached|exhausted)/i,
|
|
46
|
+
/resets? (?:at|in)\b/i,
|
|
47
|
+
/resets? \d{1,2}(?::\d{2})?\s*(?:am|pm)/i
|
|
48
|
+
// "resets 4:10am" — no "at" (live form)
|
|
49
|
+
];
|
|
50
|
+
var ALS = new AsyncLocalStorage();
|
|
51
|
+
|
|
52
|
+
// ../coding-core/dist/messages.js
|
|
53
|
+
import { promises as fs } from "fs";
|
|
54
|
+
|
|
55
|
+
// ../coding-core/dist/injected.js
|
|
56
|
+
var SYSTEM_REMINDER_RE = /<system-reminder>[\s\S]*?<\/system-reminder>/g;
|
|
57
|
+
var INJECTED_RES = [
|
|
58
|
+
/<task-notification>[\s\S]*?<\/task-notification>/g,
|
|
59
|
+
/<local-command-stdout>[\s\S]*?<\/local-command-stdout>/g,
|
|
60
|
+
/<local-command-caveat>[\s\S]*?<\/local-command-caveat>/g,
|
|
61
|
+
/<ide_opened_file>[\s\S]*?<\/ide_opened_file>/g
|
|
62
|
+
];
|
|
63
|
+
var HARNESS_TURN_RES = [
|
|
64
|
+
/^Non user activity$/i,
|
|
65
|
+
/^<<autonomous-loop(-dynamic)?>>$/,
|
|
66
|
+
/^This session is being continued from a previous conversation/,
|
|
67
|
+
// the loop skill's expanded wake-up prompt — older CLI versions wrote it as a
|
|
68
|
+
// plain user turn with no isMeta flag (found counting 601 words, 2026-07-07)
|
|
69
|
+
/^(-{3,}\s*)?#\s*Autonomous loop (check|tick)/
|
|
70
|
+
];
|
|
71
|
+
function stripInjected(t) {
|
|
72
|
+
for (const re of INJECTED_RES)
|
|
73
|
+
t = t.replace(re, "");
|
|
74
|
+
return t.replace(SYSTEM_REMINDER_RE, "");
|
|
75
|
+
}
|
|
76
|
+
function isHarnessTurn(t) {
|
|
77
|
+
const s = t.trim();
|
|
78
|
+
return HARNESS_TURN_RES.some((re) => re.test(s));
|
|
79
|
+
}
|
|
80
|
+
function humanTypedText(t) {
|
|
81
|
+
const s = stripInjected(t).trim();
|
|
82
|
+
return s && !isHarnessTurn(s) ? s : "";
|
|
83
|
+
}
|
|
84
|
+
function isHarnessEntry(e) {
|
|
85
|
+
return e.isMeta === true || e.isCompactSummary === true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ../coding-core/dist/messages.js
|
|
89
|
+
function extractText(content) {
|
|
90
|
+
if (typeof content === "string")
|
|
91
|
+
return content;
|
|
92
|
+
if (Array.isArray(content))
|
|
93
|
+
return content.filter((c) => c && typeof c === "object" && c.type === "text").map((c) => c.text || "").join("\n");
|
|
94
|
+
return "";
|
|
95
|
+
}
|
|
96
|
+
function isToolResultOnly(content) {
|
|
97
|
+
return Array.isArray(content) && content.length > 0 && content.every((c) => c && typeof c === "object" && c.type === "tool_result");
|
|
98
|
+
}
|
|
99
|
+
async function extractMessages(file) {
|
|
100
|
+
let raw;
|
|
101
|
+
try {
|
|
102
|
+
raw = await fs.readFile(file, "utf-8");
|
|
103
|
+
} catch {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
const out = [];
|
|
107
|
+
const userTexts = /* @__PURE__ */ new Set();
|
|
108
|
+
const seenUuids = /* @__PURE__ */ new Set();
|
|
109
|
+
let aiProse = [];
|
|
110
|
+
let aiTools = {};
|
|
111
|
+
let aiStart = 0;
|
|
112
|
+
const flushAI = () => {
|
|
113
|
+
if (!aiProse.length && Object.keys(aiTools).length === 0)
|
|
114
|
+
return;
|
|
115
|
+
const toolStr = Object.entries(aiTools).map(([n, c]) => c > 1 ? `${n}\xD7${c}` : n).join(", ");
|
|
116
|
+
let prose = aiProse.join(" ").replace(/\s+/g, " ").trim();
|
|
117
|
+
if (prose.length > 2200)
|
|
118
|
+
prose = prose.slice(0, 2200) + "\u2026";
|
|
119
|
+
let text = prose;
|
|
120
|
+
if (toolStr)
|
|
121
|
+
text = (text ? text + " " : "") + `[ran ${toolStr}]`;
|
|
122
|
+
if (text)
|
|
123
|
+
out.push({ t: aiStart, role: "ai", text });
|
|
124
|
+
aiProse = [];
|
|
125
|
+
aiTools = {};
|
|
126
|
+
};
|
|
127
|
+
for (const line of raw.split("\n")) {
|
|
128
|
+
const s = line.trim();
|
|
129
|
+
if (!s)
|
|
130
|
+
continue;
|
|
131
|
+
let e;
|
|
132
|
+
try {
|
|
133
|
+
e = JSON.parse(s);
|
|
134
|
+
} catch {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const t = typeof e.timestamp === "string" ? Date.parse(e.timestamp) : NaN;
|
|
138
|
+
if (!Number.isFinite(t))
|
|
139
|
+
continue;
|
|
140
|
+
if (e.type === "queue-operation" && e.operation === "enqueue" && typeof e.content === "string") {
|
|
141
|
+
const qt = humanTypedText(e.content);
|
|
142
|
+
if (qt) {
|
|
143
|
+
flushAI();
|
|
144
|
+
out.push({ t, role: "user", text: qt, q: true });
|
|
145
|
+
}
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (e.type === "user") {
|
|
149
|
+
if (isHarnessEntry(e))
|
|
150
|
+
continue;
|
|
151
|
+
const uuid = typeof e.uuid === "string" ? e.uuid : "";
|
|
152
|
+
if (uuid && seenUuids.has(uuid))
|
|
153
|
+
continue;
|
|
154
|
+
const content = e.message?.content;
|
|
155
|
+
if (isToolResultOnly(content))
|
|
156
|
+
continue;
|
|
157
|
+
const text = humanTypedText(extractText(content));
|
|
158
|
+
if (text) {
|
|
159
|
+
if (uuid)
|
|
160
|
+
seenUuids.add(uuid);
|
|
161
|
+
userTexts.add(text);
|
|
162
|
+
flushAI();
|
|
163
|
+
out.push({ t, role: "user", text });
|
|
164
|
+
}
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (e.type === "assistant") {
|
|
168
|
+
const msg = e.message ?? {};
|
|
169
|
+
if (!aiProse.length && Object.keys(aiTools).length === 0)
|
|
170
|
+
aiStart = t;
|
|
171
|
+
if (Array.isArray(msg.content)) {
|
|
172
|
+
for (const item of msg.content) {
|
|
173
|
+
const it = item;
|
|
174
|
+
if (it.type === "text" && it.text)
|
|
175
|
+
aiProse.push(it.text);
|
|
176
|
+
else if (it.type === "tool_use" && it.name)
|
|
177
|
+
aiTools[it.name] = (aiTools[it.name] ?? 0) + 1;
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
const p = extractText(msg.content);
|
|
181
|
+
if (p)
|
|
182
|
+
aiProse.push(p);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
flushAI();
|
|
187
|
+
return out.filter((m) => !(m.q && userTexts.has(m.text))).map(({ q, ...m }) => m).sort((a, b) => a.t - b.t);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ../coding-core/dist/words.js
|
|
191
|
+
var CODE_START = /^\s*(import |export |const |let |var |function\b|class |def |async |await |return |if\s*\(|for\s*\(|while\s*\(|switch\s*\(|#include|<\/?[a-zA-Z][\w-]*[\s>]|[}{)\]]|@[A-Za-z]|\$\s|npm |npx |git |cd |sudo )/;
|
|
192
|
+
var SEAM_RE = /\n\s*\n\s*\n+/;
|
|
193
|
+
var SEGMENT_CEILING = 400;
|
|
194
|
+
var NOTE = "((ran|created|edited|read|wrote|searched) (a|an|\\d+) [a-z]+|updated todos)";
|
|
195
|
+
var TRANSCRIPT_LINE_RE = new RegExp(`^\\s*(${NOTE})((,| and) ${NOTE})*\\s*$|^\\s*thought for .{1,30}$`, "im");
|
|
196
|
+
var EM_DASH_RE = / — /g;
|
|
197
|
+
var MAX_WPM = 1e3;
|
|
198
|
+
var DEFAULT_GAP_MIN = 5;
|
|
199
|
+
function typedWords(text, gapMs) {
|
|
200
|
+
const typed = humanTypedText(text);
|
|
201
|
+
const msgDashes = (typed.match(EM_DASH_RE) || []).length;
|
|
202
|
+
const msgWords = proseWords(typed);
|
|
203
|
+
const draftedMsg = msgDashes >= 6 && msgWords > 0 && msgDashes / msgWords >= 1 / 200;
|
|
204
|
+
let counted = 0;
|
|
205
|
+
for (const seg of typed.split(SEAM_RE)) {
|
|
206
|
+
if (TRANSCRIPT_LINE_RE.test(seg))
|
|
207
|
+
continue;
|
|
208
|
+
const w = proseWords(seg);
|
|
209
|
+
if (!w)
|
|
210
|
+
continue;
|
|
211
|
+
const dashes = (seg.match(EM_DASH_RE) || []).length;
|
|
212
|
+
if (dashes >= 3 && dashes / w >= 1 / 200)
|
|
213
|
+
continue;
|
|
214
|
+
if (draftedMsg && dashes >= 1)
|
|
215
|
+
continue;
|
|
216
|
+
counted += Math.min(w, SEGMENT_CEILING);
|
|
217
|
+
}
|
|
218
|
+
const mins = gapMs / 6e4;
|
|
219
|
+
const window = Number.isFinite(mins) && mins > 0 ? mins : DEFAULT_GAP_MIN;
|
|
220
|
+
return Math.min(counted, Math.round(MAX_WPM * window));
|
|
221
|
+
}
|
|
222
|
+
function proseWords(text) {
|
|
223
|
+
let t = text;
|
|
224
|
+
const skillAt = t.indexOf("Base directory for this skill:");
|
|
225
|
+
if (skillAt >= 0)
|
|
226
|
+
t = t.slice(0, skillAt);
|
|
227
|
+
const contAt = t.indexOf("This session is being continued from a previous conversation");
|
|
228
|
+
if (contAt >= 0)
|
|
229
|
+
t = t.slice(0, contAt);
|
|
230
|
+
t = t.replace(/<command-[a-z-]+>[\s\S]*?<\/command-[a-z-]+>/gi, " ");
|
|
231
|
+
t = t.replace(/```[\s\S]*?```/g, " ");
|
|
232
|
+
t = t.replace(/`[^`\n]{2,}`/g, " ");
|
|
233
|
+
const kept = t.split("\n").filter((ln) => {
|
|
234
|
+
const s = ln.trim();
|
|
235
|
+
if (s.length < 8)
|
|
236
|
+
return true;
|
|
237
|
+
const letters = (s.match(/[A-Za-z]/g) || []).length;
|
|
238
|
+
const symbols = (s.match(/[{}()[\];=<>+\-*/%&|^~$@\\]/g) || []).length;
|
|
239
|
+
const codey = CODE_START.test(s) || symbols > 3 && symbols / s.length > 0.25 || // symbol-dense → code
|
|
240
|
+
letters / s.length < 0.45;
|
|
241
|
+
return !codey;
|
|
242
|
+
});
|
|
243
|
+
return (kept.join(" ").trim().match(/\S+/g) || []).length;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ../../scripts/coding-gap-dist.mts
|
|
247
|
+
var CODING = ".data/coding";
|
|
248
|
+
var WPM = 130;
|
|
249
|
+
var BIN = 0.5;
|
|
250
|
+
var MAX = 20;
|
|
251
|
+
async function main() {
|
|
252
|
+
const sess = JSON.parse(await fs2.readFile(path.join(CODING, "sessions.json"), "utf8")).filter((s) => !s.duplicateOf && s.klass === "interactive");
|
|
253
|
+
const gaps = [];
|
|
254
|
+
for (const s of sess) {
|
|
255
|
+
let ms = [];
|
|
256
|
+
try {
|
|
257
|
+
ms = await extractMessages(s.file);
|
|
258
|
+
} catch {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const ev = ms.map((m) => ({ t: m.t, role: m.role, words: m.role === "user" ? typedWords(m.text || "", NaN) : 0 })).sort((a, b) => a.t - b.t);
|
|
262
|
+
for (let i = 1; i < ev.length; i++) {
|
|
263
|
+
const g = (ev[i].t - ev[i - 1].t) / 6e4;
|
|
264
|
+
if (g <= 0 || g > 60) continue;
|
|
265
|
+
gaps.push(Math.max(0, g - (ev[i].role === "user" ? ev[i].words / WPM : 0)));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
gaps.sort((a, b) => a - b);
|
|
269
|
+
const bins = [];
|
|
270
|
+
for (let lo = 0; lo < MAX; lo = Math.round((lo + BIN) * 10) / 10) bins.push({ lo, hi: Math.round((lo + BIN) * 10) / 10, count: 0 });
|
|
271
|
+
let over = 0;
|
|
272
|
+
for (const g of gaps) {
|
|
273
|
+
if (g >= MAX) {
|
|
274
|
+
over++;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
bins[Math.floor(g / BIN)].count++;
|
|
278
|
+
}
|
|
279
|
+
const curve = [];
|
|
280
|
+
for (let t = 0.5; t <= 15; t = Math.round((t + 0.5) * 10) / 10) curve.push({ t, fracUnder: Math.round(gaps.filter((g) => g < t).length / gaps.length * 1e3) / 1e3 });
|
|
281
|
+
const pct = (p) => gaps[Math.min(gaps.length - 1, Math.floor(gaps.length * p))] ?? 0;
|
|
282
|
+
const out = { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), totalGaps: gaps.length, binMin: BIN, maxMin: MAX, bins, over, currentFlowGap: 5, curve, median: Math.round(pct(0.5) * 100) / 100, p25: Math.round(pct(0.25) * 100) / 100, p75: Math.round(pct(0.75) * 100) / 100, p90: Math.round(pct(0.9) * 100) / 100 };
|
|
283
|
+
await fs2.writeFile(path.join(CODING, "gap-dist.json"), JSON.stringify(out, null, 2));
|
|
284
|
+
const mx = Math.max(...bins.map((b) => b.count), 1);
|
|
285
|
+
console.log(`gap-dist: ${gaps.length} gaps \xB7 median ${out.median}m \xB7 p75 ${out.p75}m \xB7 p90 ${out.p90}m \xB7 ${over} over ${MAX}m`);
|
|
286
|
+
bins.forEach((b) => {
|
|
287
|
+
if (b.count) console.log(` ${b.lo.toFixed(1).padStart(4)}-${b.hi.toFixed(1)}m ${"\u2588".repeat(Math.round(b.count / mx * 50))} ${b.count}`);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
main().catch((e) => {
|
|
291
|
+
console.error(e);
|
|
292
|
+
process.exit(1);
|
|
293
|
+
});
|