@temet/cli 0.2.0 → 0.3.1

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 (38) hide show
  1. package/dist/audit.d.ts +34 -0
  2. package/dist/audit.js +531 -0
  3. package/dist/index.js +85 -40
  4. package/dist/lib/analysis-types.d.ts +7 -0
  5. package/dist/lib/analysis-types.js +1 -0
  6. package/dist/lib/audit-tracking.d.ts +55 -0
  7. package/dist/lib/audit-tracking.js +185 -0
  8. package/dist/lib/cli-args.d.ts +22 -0
  9. package/dist/lib/cli-args.js +65 -0
  10. package/dist/lib/editorial-taxonomy.d.ts +17 -0
  11. package/dist/lib/editorial-taxonomy.js +91 -0
  12. package/dist/lib/heuristics.d.ts +15 -0
  13. package/dist/lib/heuristics.js +341 -0
  14. package/dist/lib/hook-installer.d.ts +13 -0
  15. package/dist/lib/hook-installer.js +130 -0
  16. package/dist/lib/narrator-lite.d.ts +25 -0
  17. package/dist/lib/narrator-lite.js +231 -0
  18. package/dist/lib/notifier.d.ts +9 -0
  19. package/dist/lib/notifier.js +57 -0
  20. package/dist/lib/path-resolver.d.ts +39 -0
  21. package/dist/lib/path-resolver.js +152 -0
  22. package/dist/lib/profile-report.d.ts +18 -0
  23. package/dist/lib/profile-report.js +148 -0
  24. package/dist/lib/report-writer.d.ts +7 -0
  25. package/dist/lib/report-writer.js +73 -0
  26. package/dist/lib/session-audit.d.ts +24 -0
  27. package/dist/lib/session-audit.js +94 -0
  28. package/dist/lib/session-parser.d.ts +35 -0
  29. package/dist/lib/session-parser.js +130 -0
  30. package/dist/lib/skill-mapper.d.ts +3 -0
  31. package/dist/lib/skill-mapper.js +173 -0
  32. package/dist/lib/skill-naming.d.ts +1 -0
  33. package/dist/lib/skill-naming.js +50 -0
  34. package/dist/lib/types.d.ts +17 -0
  35. package/dist/lib/types.js +2 -0
  36. package/dist/lib/workflow-detector.d.ts +11 -0
  37. package/dist/lib/workflow-detector.js +125 -0
  38. package/package.json +2 -2
@@ -0,0 +1,231 @@
1
+ // ---------- Excerpt Selection ----------
2
+ const CORRECTION_RX = [
3
+ /\bnon\b/i,
4
+ /\bpas comme [cç]a\b/i,
5
+ /\bc'est pas\b/i,
6
+ /\bplut[oô]t\b/i,
7
+ /\brevert\b/i,
8
+ /\bwrong\b/i,
9
+ /\bnot like that\b/i,
10
+ /\bundo\b/i,
11
+ /\bactually\b/i,
12
+ /\binstead\b/i,
13
+ ];
14
+ const DECISION_RX = [
15
+ /je pr[eé]f[eè]re/i,
16
+ /toujours\s/i,
17
+ /jamais\s/i,
18
+ /il faut/i,
19
+ /I prefer/i,
20
+ /always\s/i,
21
+ /never\s/i,
22
+ /we should/i,
23
+ /make sure to/i,
24
+ ];
25
+ function getTextContent(msg) {
26
+ return msg.content
27
+ .filter((b) => b.type === "text")
28
+ .map((b) => b.text)
29
+ .join("\n");
30
+ }
31
+ function scoreMessage(msg) {
32
+ const text = getTextContent(msg);
33
+ if (!text)
34
+ return 0;
35
+ let score = 0;
36
+ for (const rx of CORRECTION_RX) {
37
+ if (rx.test(text)) {
38
+ score += 3;
39
+ break;
40
+ }
41
+ }
42
+ for (const rx of DECISION_RX) {
43
+ if (rx.test(text)) {
44
+ score += 2;
45
+ break;
46
+ }
47
+ }
48
+ if (text.length > 500)
49
+ score += 1;
50
+ return score;
51
+ }
52
+ function selectExcerpts(combined, max = 10) {
53
+ return combined.messages
54
+ .filter((m) => m.role === "user")
55
+ .map((m) => ({ msg: m, score: scoreMessage(m) }))
56
+ .filter((s) => s.score > 0)
57
+ .sort((a, b) => b.score - a.score)
58
+ .slice(0, max)
59
+ .map((s) => ({
60
+ text: getTextContent(s.msg).slice(0, 300),
61
+ role: s.msg.role,
62
+ }));
63
+ }
64
+ // ---------- Prompt ----------
65
+ function buildPrompt(raw, excerpts, workflows) {
66
+ const competencyList = raw
67
+ .map((c) => `- ${c.name} [${c.proficiencyLevel}] (${c.category}): ${c.description}`)
68
+ .join("\n");
69
+ const excerptList = excerpts
70
+ .map((e, i) => `${i + 1}. "${e.text}"`)
71
+ .join("\n");
72
+ const workflowList = workflows.length > 0
73
+ ? workflows
74
+ .map((w) => `- ${w.description}: ${w.sequence.join(" → ")} (${w.occurrences}x across ${w.sessions} sessions)`)
75
+ .join("\n")
76
+ : "None detected.";
77
+ return `You are analyzing a developer's coding sessions to produce a professional competency profile.
78
+
79
+ ## Raw Competencies (from heuristic analysis)
80
+ ${competencyList}
81
+
82
+ ## User Excerpts (from sessions)
83
+ ${excerptList}
84
+
85
+ ## Detected Workflows
86
+ ${workflowList}
87
+
88
+ ## Instructions
89
+
90
+ Return a JSON object with exactly two keys:
91
+
92
+ 1. "competencies": an array of enriched competency objects. For each raw competency above, produce an object with these fields:
93
+ - "id": same as raw
94
+ - "name": same as raw
95
+ - "category": same as raw
96
+ - "proficiencyLevel": same as raw (adjust only if evidence strongly supports a different level)
97
+ - "description": a 1-2 sentence narrative description (not the raw heuristic output)
98
+ - "evidence": object with:
99
+ - "examples": array of 2-4 concrete examples drawn from the excerpts/workflows
100
+ - "decisionCriteria": array of 1-3 decision principles the developer follows
101
+ - "antiPatterns": array of 1-2 things the developer avoids
102
+ - "mentorAdvice": array of 1-2 insights about how this developer works
103
+ - "prerequisites": array of related skills needed (0-3)
104
+ - "synergies": array of complementary skills (0-3)
105
+
106
+ 2. "bilan": a markdown narrative summary (300-600 words) covering:
107
+ - Developer profile overview
108
+ - Key strengths and surprising patterns
109
+ - Workflow patterns and methodology
110
+ - Progression areas and recommendations
111
+
112
+ Keep descriptions professional but human. Ground everything in the evidence provided.
113
+ Return ONLY valid JSON, no markdown fences.`;
114
+ }
115
+ // ---------- Response Parser ----------
116
+ function ensureStringArray(value) {
117
+ if (!Array.isArray(value))
118
+ return [];
119
+ return value.filter((v) => typeof v === "string");
120
+ }
121
+ function parseResponse(text, fallback) {
122
+ try {
123
+ const cleaned = text
124
+ .replace(/^```json?\s*/m, "")
125
+ .replace(/```\s*$/m, "")
126
+ .trim();
127
+ const parsed = JSON.parse(cleaned);
128
+ if (!parsed.competencies ||
129
+ !Array.isArray(parsed.competencies) ||
130
+ !parsed.bilan ||
131
+ typeof parsed.bilan !== "string") {
132
+ return { competencies: fallback, bilan: "" };
133
+ }
134
+ const validated = [];
135
+ for (const c of parsed.competencies) {
136
+ if (!c ||
137
+ typeof c !== "object" ||
138
+ !c.id ||
139
+ !c.name ||
140
+ !c.category ||
141
+ !c.proficiencyLevel)
142
+ continue;
143
+ const ev = c.evidence && typeof c.evidence === "object" ? c.evidence : {};
144
+ validated.push({
145
+ ...c,
146
+ description: typeof c.description === "string" ? c.description : "",
147
+ evidence: {
148
+ examples: ensureStringArray(ev.examples),
149
+ decisionCriteria: ensureStringArray(ev.decisionCriteria),
150
+ antiPatterns: ensureStringArray(ev.antiPatterns),
151
+ mentorAdvice: ensureStringArray(ev.mentorAdvice),
152
+ },
153
+ prerequisites: ensureStringArray(c.prerequisites),
154
+ synergies: ensureStringArray(c.synergies),
155
+ });
156
+ }
157
+ if (validated.length === 0)
158
+ return { competencies: fallback, bilan: parsed.bilan ?? "" };
159
+ return { competencies: validated, bilan: parsed.bilan };
160
+ }
161
+ catch {
162
+ return { competencies: fallback, bilan: "" };
163
+ }
164
+ }
165
+ export function resolveAuth() {
166
+ const claudeToken = process.env.CLAUDE_AUTH_TOKEN;
167
+ const claudeBase = process.env.CLAUDE_API_BASE_URL;
168
+ // Primary: Claude bearer proxy (same path as the app's provider chain)
169
+ if (claudeToken) {
170
+ const base = claudeBase ?? "https://api.anthropic.com";
171
+ const url = base.includes("/v1/messages") ? base : `${base}/v1/messages`;
172
+ return {
173
+ url,
174
+ headers: {
175
+ "Content-Type": "application/json",
176
+ Authorization: `Bearer ${claudeToken}`,
177
+ "anthropic-version": "2023-06-01",
178
+ },
179
+ source: "CLAUDE_AUTH_TOKEN",
180
+ };
181
+ }
182
+ // Fallback: direct Anthropic API key
183
+ const apiKey = process.env.ANTHROPIC_API_KEY;
184
+ if (apiKey) {
185
+ return {
186
+ url: "https://api.anthropic.com/v1/messages",
187
+ headers: {
188
+ "Content-Type": "application/json",
189
+ "x-api-key": apiKey,
190
+ "anthropic-version": "2023-06-01",
191
+ },
192
+ source: "ANTHROPIC_API_KEY",
193
+ };
194
+ }
195
+ return null;
196
+ }
197
+ // ---------- API Call ----------
198
+ export async function narrateCompetencies(raw, combined, workflows, options) {
199
+ const auth = resolveAuth();
200
+ if (!auth) {
201
+ console.error("[temet] --narrate requires model access. Set ANTHROPIC_API_KEY or CLAUDE_AUTH_TOKEN.");
202
+ return { competencies: raw, bilan: "" };
203
+ }
204
+ const model = options?.model ?? "claude-haiku-4-5-20251001";
205
+ const excerpts = selectExcerpts(combined);
206
+ const prompt = buildPrompt(raw, excerpts, workflows);
207
+ console.error(`[temet] narrator using ${auth.source}`);
208
+ try {
209
+ const resp = await fetch(auth.url, {
210
+ method: "POST",
211
+ headers: auth.headers,
212
+ body: JSON.stringify({
213
+ model,
214
+ max_tokens: 4096,
215
+ messages: [{ role: "user", content: prompt }],
216
+ }),
217
+ });
218
+ if (!resp.ok) {
219
+ const body = await resp.text();
220
+ console.error(`[temet] narrator API error (${resp.status}): ${body}`);
221
+ return { competencies: raw, bilan: "" };
222
+ }
223
+ const data = (await resp.json());
224
+ const text = data.content?.find((b) => b.type === "text")?.text ?? "";
225
+ return parseResponse(text, raw);
226
+ }
227
+ catch (err) {
228
+ console.error("[temet] narrator call failed:", err);
229
+ return { competencies: raw, bilan: "" };
230
+ }
231
+ }
@@ -0,0 +1,9 @@
1
+ import type { AuditChange } from "./audit-tracking.js";
2
+ export type NotificationPayload = {
3
+ title: string;
4
+ body: string;
5
+ };
6
+ /** Build the notification text from audit changes. Returns null if nothing to notify. */
7
+ export declare function formatNotification(changes: AuditChange[], projectLabel: string): NotificationPayload | null;
8
+ /** Send a native OS notification. Fire-and-forget. */
9
+ export declare function sendNotification(payload: NotificationPayload): Promise<void>;
@@ -0,0 +1,57 @@
1
+ import { execFile } from "node:child_process";
2
+ import { appendFile, mkdir } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import path from "node:path";
5
+ /** Build the notification text from audit changes. Returns null if nothing to notify. */
6
+ export function formatNotification(changes, projectLabel) {
7
+ if (changes.length === 0)
8
+ return null;
9
+ // Baseline
10
+ if (changes.length === 1 && changes[0].type === "baseline") {
11
+ const skillCount = changes[0].detail.match(/(\d+)/)?.[1] ?? "0";
12
+ return {
13
+ title: "Temet",
14
+ body: `Tracking started for ${projectLabel}`,
15
+ };
16
+ }
17
+ const meaningful = changes.filter((c) => c.type !== "baseline");
18
+ if (meaningful.length === 0)
19
+ return null;
20
+ const first = meaningful[0];
21
+ const body = meaningful.length === 1
22
+ ? first.title
23
+ : `${first.title} and ${meaningful.length - 1} more`;
24
+ return { title: "Temet", body };
25
+ }
26
+ /** Send a native OS notification. Fire-and-forget. */
27
+ export async function sendNotification(payload) {
28
+ const { title, body } = payload;
29
+ if (process.platform === "darwin") {
30
+ return new Promise((resolve, reject) => {
31
+ const escaped = body.replace(/"/g, '\\"');
32
+ const script = `display notification "${escaped}" with title "${title.replace(/"/g, '\\"')}"`;
33
+ execFile("osascript", ["-e", script], (err) => {
34
+ if (err)
35
+ reject(err);
36
+ else
37
+ resolve();
38
+ });
39
+ });
40
+ }
41
+ if (process.platform === "linux") {
42
+ return new Promise((resolve, reject) => {
43
+ execFile("notify-send", [title, body], (err) => {
44
+ if (err)
45
+ reject(err);
46
+ else
47
+ resolve();
48
+ });
49
+ });
50
+ }
51
+ // Fallback: log to file
52
+ const logDir = path.join(homedir(), ".temet");
53
+ await mkdir(logDir, { recursive: true });
54
+ const logPath = path.join(logDir, "notifications.log");
55
+ const line = `[${new Date().toISOString()}] ${title}: ${body}\n`;
56
+ await appendFile(logPath, line, "utf8");
57
+ }
@@ -0,0 +1,39 @@
1
+ export type SessionCandidate = {
2
+ sessionDir: string;
3
+ label: string;
4
+ updatedAtMs: number;
5
+ };
6
+ export type SessionResolution = {
7
+ path: string | null;
8
+ source: "explicit" | "env" | "stdin" | "cwd" | "recent" | "none";
9
+ candidates: SessionCandidate[];
10
+ };
11
+ type ResolveSessionPathOptions = {
12
+ cwd?: string;
13
+ claudeRoot?: string;
14
+ hookInput?: {
15
+ cwd?: string;
16
+ } | null;
17
+ };
18
+ /** Convert an absolute project directory to the Claude Code session directory slug. */
19
+ export declare function cwdToSessionDir(cwd: string): string;
20
+ export declare function humanizeSessionDir(sessionDir: string): string;
21
+ export declare function findClaudeProjectCandidates(claudeRoot?: string): SessionCandidate[];
22
+ /**
23
+ * Try to read the JSON stdin from a Claude Code hook (non-blocking).
24
+ * Only safe to call when we know stdin has finite piped data (hook context).
25
+ * Consumes stdin — call at most once per process.
26
+ */
27
+ export declare function readHookStdin(): {
28
+ cwd?: string;
29
+ } | null;
30
+ /**
31
+ * Resolve the session path by priority:
32
+ * 1. Explicit --path value
33
+ * 2. $CLAUDE_PROJECT_DIR env var
34
+ * 3. stdin JSON cwd (hook context)
35
+ * 4. process.cwd()
36
+ * 5. most recent valid Claude project
37
+ */
38
+ export declare function resolveSessionPath(explicit: string | undefined, env: NodeJS.ProcessEnv, options?: ResolveSessionPathOptions): SessionResolution;
39
+ export {};
@@ -0,0 +1,152 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import path from "node:path";
4
+ const DEFAULT_CLAUDE_ROOT = path.join(homedir(), ".claude", "projects");
5
+ /** Convert an absolute project directory to the Claude Code session directory slug. */
6
+ export function cwdToSessionDir(cwd) {
7
+ const slug = path.resolve(cwd).replace(/\//g, "-");
8
+ return path.join(DEFAULT_CLAUDE_ROOT, slug);
9
+ }
10
+ export function humanizeSessionDir(sessionDir) {
11
+ const slug = path.basename(sessionDir);
12
+ if (!slug)
13
+ return sessionDir;
14
+ if (slug.startsWith("-")) {
15
+ return slug.replace(/-/g, "/");
16
+ }
17
+ return slug;
18
+ }
19
+ function hasJsonlSessions(dir) {
20
+ try {
21
+ for (const entry of readdirSync(dir)) {
22
+ const full = path.join(dir, entry);
23
+ const stat = statSync(full);
24
+ if (stat.isDirectory()) {
25
+ if (hasJsonlSessions(full))
26
+ return true;
27
+ continue;
28
+ }
29
+ if (stat.isFile() && entry.endsWith(".jsonl"))
30
+ return true;
31
+ }
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ return false;
37
+ }
38
+ function candidateFromSessionDir(sessionDir) {
39
+ if (!existsSync(sessionDir) || !hasJsonlSessions(sessionDir))
40
+ return null;
41
+ let updatedAtMs = 0;
42
+ try {
43
+ updatedAtMs = statSync(sessionDir).mtimeMs;
44
+ }
45
+ catch {
46
+ updatedAtMs = 0;
47
+ }
48
+ return {
49
+ sessionDir,
50
+ label: humanizeSessionDir(sessionDir),
51
+ updatedAtMs,
52
+ };
53
+ }
54
+ export function findClaudeProjectCandidates(claudeRoot = DEFAULT_CLAUDE_ROOT) {
55
+ try {
56
+ return readdirSync(claudeRoot)
57
+ .map((entry) => path.join(claudeRoot, entry))
58
+ .filter((full) => {
59
+ try {
60
+ return statSync(full).isDirectory();
61
+ }
62
+ catch {
63
+ return false;
64
+ }
65
+ })
66
+ .map(candidateFromSessionDir)
67
+ .filter((candidate) => candidate !== null)
68
+ .sort((a, b) => b.updatedAtMs - a.updatedAtMs);
69
+ }
70
+ catch {
71
+ return [];
72
+ }
73
+ }
74
+ /**
75
+ * Try to read the JSON stdin from a Claude Code hook (non-blocking).
76
+ * Only safe to call when we know stdin has finite piped data (hook context).
77
+ * Consumes stdin — call at most once per process.
78
+ */
79
+ export function readHookStdin() {
80
+ try {
81
+ if (process.stdin.isTTY)
82
+ return null;
83
+ if (!process.env.CLAUDECODE)
84
+ return null;
85
+ const data = readFileSync(0, "utf8");
86
+ return JSON.parse(data);
87
+ }
88
+ catch {
89
+ return null;
90
+ }
91
+ }
92
+ /**
93
+ * Resolve the session path by priority:
94
+ * 1. Explicit --path value
95
+ * 2. $CLAUDE_PROJECT_DIR env var
96
+ * 3. stdin JSON cwd (hook context)
97
+ * 4. process.cwd()
98
+ * 5. most recent valid Claude project
99
+ */
100
+ export function resolveSessionPath(explicit, env, options = {}) {
101
+ if (explicit) {
102
+ return {
103
+ path: explicit,
104
+ source: "explicit",
105
+ candidates: [],
106
+ };
107
+ }
108
+ const cwd = options.cwd ?? process.cwd();
109
+ const claudeRoot = options.claudeRoot ?? DEFAULT_CLAUDE_ROOT;
110
+ const fromEnv = env.CLAUDE_PROJECT_DIR
111
+ ? candidateFromSessionDir(path.join(claudeRoot, path.resolve(env.CLAUDE_PROJECT_DIR).replace(/\//g, "-")))
112
+ : null;
113
+ if (fromEnv) {
114
+ return {
115
+ path: fromEnv.sessionDir,
116
+ source: "env",
117
+ candidates: [fromEnv],
118
+ };
119
+ }
120
+ const hookInput = options.hookInput ?? readHookStdin();
121
+ const fromStdin = hookInput?.cwd
122
+ ? candidateFromSessionDir(path.join(claudeRoot, path.resolve(hookInput.cwd).replace(/\//g, "-")))
123
+ : null;
124
+ if (fromStdin) {
125
+ return {
126
+ path: fromStdin.sessionDir,
127
+ source: "stdin",
128
+ candidates: [fromStdin],
129
+ };
130
+ }
131
+ const fromCwd = candidateFromSessionDir(path.join(claudeRoot, path.resolve(cwd).replace(/\//g, "-")));
132
+ if (fromCwd) {
133
+ return {
134
+ path: fromCwd.sessionDir,
135
+ source: "cwd",
136
+ candidates: [fromCwd],
137
+ };
138
+ }
139
+ const candidates = findClaudeProjectCandidates(claudeRoot);
140
+ if (candidates.length > 0) {
141
+ return {
142
+ path: candidates[0].sessionDir,
143
+ source: "recent",
144
+ candidates,
145
+ };
146
+ }
147
+ return {
148
+ path: null,
149
+ source: "none",
150
+ candidates: [],
151
+ };
152
+ }
@@ -0,0 +1,18 @@
1
+ import type { TrackingResult } from "./audit-tracking.js";
2
+ import type { AuditResult } from "./session-audit.js";
3
+ import type { CompetencyEntry } from "./types.js";
4
+ export type NarrativeSection = {
5
+ title: string;
6
+ tagline?: string;
7
+ body: string;
8
+ };
9
+ export type SkillAuditReport = {
10
+ title: string;
11
+ subtitle: string;
12
+ tacitSkills: NarrativeSection[];
13
+ blindSpots: NarrativeSection[];
14
+ professionalDna: string;
15
+ metaSkill: NarrativeSection;
16
+ meaningfulChanges: string[];
17
+ };
18
+ export declare function buildSkillAuditReport(result: Pick<AuditResult, "sessionCount" | "promptCount" | "toolCallCount" | "workflowCount" | "signals">, competencies: CompetencyEntry[], tracking?: Pick<TrackingResult, "changes">): SkillAuditReport;
@@ -0,0 +1,148 @@
1
+ import { extractEditorialEvidence, fallbackEditorialSummary, isCommoditySkillName, matchEditorialSkill, } from "./editorial-taxonomy.js";
2
+ const PROFICIENCY_SCORE = {
3
+ expert: 5,
4
+ proficient: 4,
5
+ competent: 3,
6
+ advanced_beginner: 2,
7
+ novice: 1,
8
+ };
9
+ function scoreCompetency(entry) {
10
+ const evidenceCount = entry.evidence.examples.length +
11
+ entry.evidence.decisionCriteria.length +
12
+ entry.evidence.antiPatterns.length +
13
+ entry.evidence.mentorAdvice.length;
14
+ return PROFICIENCY_SCORE[entry.proficiencyLevel] * 10 + evidenceCount;
15
+ }
16
+ function pickTacitSkills(competencies) {
17
+ const sorted = [...competencies].sort((a, b) => scoreCompetency(b) - scoreCompetency(a));
18
+ const usedTitles = new Set();
19
+ const sections = [];
20
+ const presetMatches = sorted.filter((entry) => matchEditorialSkill(entry));
21
+ const fallbackCandidates = sorted.filter((entry) => !matchEditorialSkill(entry) && !isCommoditySkillName(entry.name));
22
+ for (const entry of presetMatches) {
23
+ const preset = matchEditorialSkill(entry);
24
+ const title = preset?.title ?? entry.name;
25
+ if (usedTitles.has(title))
26
+ continue;
27
+ usedTitles.add(title);
28
+ const evidence = extractEditorialEvidence(entry);
29
+ sections.push({
30
+ title,
31
+ tagline: preset?.tagline,
32
+ body: preset
33
+ ? preset.render(entry, evidence)
34
+ : fallbackEditorialSummary(entry),
35
+ });
36
+ if (sections.length >= 10)
37
+ break;
38
+ }
39
+ if (sections.length < 6) {
40
+ for (const entry of fallbackCandidates) {
41
+ const preset = matchEditorialSkill(entry);
42
+ const title = preset?.title ?? entry.name;
43
+ if (usedTitles.has(title))
44
+ continue;
45
+ usedTitles.add(title);
46
+ const evidence = extractEditorialEvidence(entry);
47
+ sections.push({
48
+ title,
49
+ tagline: preset?.tagline,
50
+ body: preset
51
+ ? preset.render(entry, evidence)
52
+ : fallbackEditorialSummary(entry),
53
+ });
54
+ if (sections.length >= 8)
55
+ break;
56
+ }
57
+ }
58
+ return sections;
59
+ }
60
+ function hasSkill(competencies, pattern) {
61
+ return competencies.some((entry) => pattern.test(entry.name.toLowerCase()));
62
+ }
63
+ function buildBlindSpots(result, competencies) {
64
+ const blindSpots = [];
65
+ if (result.workflowCount >= 80) {
66
+ blindSpots.push({
67
+ title: "Feature sprawl",
68
+ body: "You can generate momentum faster than you stabilize it. When the number of patterns and directions keeps climbing, the risk is not lack of output. It is losing the discipline to decide what deserves to become a finished path.",
69
+ });
70
+ }
71
+ if (hasSkill(competencies, /version control/) ||
72
+ result.signals.some((signal) => /version control/i.test(signal.skill))) {
73
+ blindSpots.push({
74
+ title: "Git hygiene under pressure",
75
+ body: "Your work suggests strong version-control awareness, but also the kind of environment where branch, commit, and release pressure can still create avoidable mess. This is usually a systems problem, not a knowledge problem.",
76
+ });
77
+ }
78
+ if (hasSkill(competencies, /research|architecture|code comprehension|refactoring/)) {
79
+ blindSpots.push({
80
+ title: "Technical detours",
81
+ body: "You are capable of going deep, which is a strength until exploration keeps running after product value has flattened. The risk is not bad engineering. It is staying fascinated one loop too long.",
82
+ });
83
+ }
84
+ const fallback = [
85
+ {
86
+ title: "Feature sprawl",
87
+ body: "You move quickly once a direction looks promising. The risk is not inactivity. It is letting the number of live possibilities grow faster than the number of finished outcomes.",
88
+ },
89
+ {
90
+ title: "Coordination drag",
91
+ body: "As your systems become more capable, the hidden cost shifts toward alignment, naming, and decision clarity. Work can stay technically correct while still becoming harder to coordinate.",
92
+ },
93
+ {
94
+ title: "Technical detours",
95
+ body: "Deep exploration is one of your strengths, but it can keep producing value long after the product question should have ended the loop. Good curiosity still needs a stopping rule.",
96
+ },
97
+ ];
98
+ for (const item of fallback) {
99
+ if (blindSpots.some((existing) => existing.title === item.title))
100
+ continue;
101
+ blindSpots.push(item);
102
+ if (blindSpots.length >= 3)
103
+ break;
104
+ }
105
+ return blindSpots.slice(0, 3);
106
+ }
107
+ function buildProfessionalDna(tacitSkills, competencies) {
108
+ const anchors = tacitSkills.slice(0, 3).map((skill) => skill.title);
109
+ const stacks = competencies
110
+ .filter((entry) => /cloudflare|react|api|type system|component|security|shell/i.test(entry.name))
111
+ .slice(0, 4)
112
+ .map((entry) => entry.name);
113
+ const stackLine = stacks.length > 0
114
+ ? `The technical signal clusters around ${stacks.join(", ")}.`
115
+ : "The technical signal clusters around architecture, implementation judgment, and repeated execution patterns.";
116
+ return `This profile points to someone who turns ambiguity into execution. The strongest signals are ${anchors.join(", ")}, which together describe a person who reads systems carefully, imposes structure early, and keeps moving through short feedback loops instead of relying on one big heroic pass. ${stackLine} The distinctive advantage is not one isolated skill, but the ability to convert messy work into something legible, directional, and reusable.`;
117
+ }
118
+ function buildMetaSkill(tacitSkills) {
119
+ const titles = tacitSkills.map((skill) => skill.title.toLowerCase());
120
+ if (titles.some((title) => title.includes("plan-first")) &&
121
+ titles.some((title) => title.includes("codebase archaeology"))) {
122
+ return {
123
+ title: "Turning ambiguity into executable systems",
124
+ body: "Across the report, the deepest pattern is not just building or reviewing. It is repeatedly taking vague, shifting work and turning it into a system that can be understood, executed, and improved. That is the skill underneath the other skills.",
125
+ };
126
+ }
127
+ return {
128
+ title: "Building feedback loops around real work",
129
+ body: "The common thread is not any one tool or framework. It is the habit of turning real work into a loop: observe, judge, change, test, and run again. That is what makes the rest of the profile coherent.",
130
+ };
131
+ }
132
+ function buildMeaningfulChanges(tracking) {
133
+ if (!tracking || tracking.changes.length === 0)
134
+ return [];
135
+ return tracking.changes.map((change) => `${change.title} — ${change.detail}`);
136
+ }
137
+ export function buildSkillAuditReport(result, competencies, tracking) {
138
+ const tacitSkills = pickTacitSkills(competencies);
139
+ return {
140
+ title: `TACIT SKILL EXTRACTION — ${result.promptCount.toLocaleString("en-US")} prompts analyzed`,
141
+ subtitle: `${result.sessionCount.toLocaleString("en-US")} sessions · ${result.toolCallCount.toLocaleString("en-US")} tool calls · ${result.workflowCount.toLocaleString("en-US")} workflow patterns. This is a deterministic Temet reading of the skills, habits, and judgment patterns that show up repeatedly in your real AI work.`,
142
+ tacitSkills,
143
+ blindSpots: buildBlindSpots(result, competencies),
144
+ professionalDna: buildProfessionalDna(tacitSkills, competencies),
145
+ metaSkill: buildMetaSkill(tacitSkills),
146
+ meaningfulChanges: buildMeaningfulChanges(tracking),
147
+ };
148
+ }
@@ -0,0 +1,7 @@
1
+ import type { AuditResult } from "./session-audit.js";
2
+ import type { TrackingResult } from "./audit-tracking.js";
3
+ import type { CompetencyEntry } from "./types.js";
4
+ export declare function buildAuditTextReport(result: Pick<AuditResult, "sessionCount" | "promptCount" | "toolCallCount" | "signals" | "workflowCount" | "workflows">, competencies: CompetencyEntry[], bilan?: string, tracking?: Pick<TrackingResult, "changes" | "current">): string;
5
+ export declare function buildReportPath(projectLabel: string, now?: Date): string;
6
+ export declare function saveAuditTextReport(projectLabel: string, content: string, now?: Date): Promise<string>;
7
+ export declare function openReportFile(filePath: string, platform?: NodeJS.Platform): Promise<void>;