etymd 0.1.0

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.
@@ -0,0 +1,444 @@
1
+ type PackageManager = "pnpm" | "yarn" | "npm" | "bun" | "unknown";
2
+ type WorkspaceKind = "pnpm" | "yarn" | "npm" | "nx" | "turbo" | "lerna" | "none";
3
+ type CiSystem = "gitlab" | "github" | "none";
4
+ /** A project script classified into the roles that make up a "Done =" definition. */
5
+ interface DiscoveredCommands {
6
+ test?: string;
7
+ lint?: string;
8
+ typecheck?: string;
9
+ format?: string;
10
+ formatCheck?: string;
11
+ build?: string;
12
+ dev?: string;
13
+ /** Every raw `scripts` key, kept so nothing is silently lost. */
14
+ raw: Record<string, string>;
15
+ }
16
+ interface PackageInfo {
17
+ name: string;
18
+ /** Path relative to the scan root. */
19
+ dir: string;
20
+ private: boolean;
21
+ version?: string;
22
+ }
23
+ /** An agent-workflow artifact etymd knows how to read or generate. */
24
+ interface DetectedArtifact {
25
+ id: string;
26
+ label: string;
27
+ path: string;
28
+ /** What role it plays: the single source of truth, a per-agent pointer, a skill, a gate, etc. */
29
+ kind: "contract" | "state" | "adapter" | "skill" | "gate" | "map" | "sessions" | "other";
30
+ exists: boolean;
31
+ }
32
+ interface GitFacts {
33
+ isRepo: boolean;
34
+ branch?: string;
35
+ head?: string;
36
+ hooksPath?: string;
37
+ husky: boolean;
38
+ /** Distinct commit authors in recent history — the solo-vs-team profile signal. */
39
+ recentAuthors?: number;
40
+ /** Ticket-key prefix inferred from recent commit subjects (e.g. "NGRE2E"). */
41
+ ticketKey?: string;
42
+ }
43
+ interface HookFacts {
44
+ /** husky-legacy = husky v3/v4 config (package.json `husky` key or husky.config.js), no .husky dir. */
45
+ source: "githooks" | "husky" | "husky-legacy" | "custom" | "none";
46
+ /** The actual hooks directory when known (covers a custom core.hooksPath). */
47
+ dir?: string;
48
+ preCommit: boolean;
49
+ prePush: boolean;
50
+ commitMsg: boolean;
51
+ lintStaged: boolean;
52
+ }
53
+ interface ProjectFacts {
54
+ etymdVersion: string;
55
+ packVersion: string;
56
+ generatedAt: string;
57
+ root: string;
58
+ name: string;
59
+ git: GitFacts;
60
+ packageManager: PackageManager;
61
+ node?: string;
62
+ workspace: {
63
+ kind: WorkspaceKind;
64
+ packageGlobs: string[];
65
+ };
66
+ packages: PackageInfo[];
67
+ frameworks: string[];
68
+ commands: DiscoveredCommands;
69
+ ci: {
70
+ system: CiSystem;
71
+ files: string[];
72
+ };
73
+ hooks: HookFacts;
74
+ artifacts: DetectedArtifact[];
75
+ tree: {
76
+ dirs: {
77
+ name: string;
78
+ files: number;
79
+ }[];
80
+ /** True when the file count hit the walk cap (large repo). */
81
+ truncated: boolean;
82
+ };
83
+ }
84
+ interface ContextFile {
85
+ path: string;
86
+ role: string;
87
+ words: number;
88
+ approxTokens: number;
89
+ }
90
+ interface ContextBudget {
91
+ files: ContextFile[];
92
+ totalWords: number;
93
+ totalApproxTokens: number;
94
+ /** The per-file threshold this measurement used (config-overridable). */
95
+ perFileWords: number;
96
+ /** Files large enough to be worth extracting into an on-demand skill. */
97
+ extractionCandidates: ContextFile[];
98
+ }
99
+
100
+ /**
101
+ * The deterministic half of a reckoning: everything knowable without an LLM. Kept pure of any
102
+ * terminal output so it can back both the CLI and programmatic use, and so it is trivially
103
+ * testable against a fixture directory.
104
+ */
105
+ declare function scanProject(root: string): Promise<ProjectFacts>;
106
+
107
+ /**
108
+ * Default word count above which a single always-loaded file is worth extracting into an
109
+ * on-demand skill. Overridable per repo via `context.perFileWords` in `.etymd/config.json`.
110
+ */
111
+ declare const EXTRACTION_THRESHOLD: number;
112
+ /**
113
+ * A Cursor rule only loads every session when it is genuinely always-applied. Scoped rules
114
+ * (globs, or alwaysApply omitted/false in frontmatter) load on demand and must not inflate the
115
+ * budget — the flagship metric has to be honest to be worth anything.
116
+ */
117
+ declare function isAlwaysAppliedCursorRule(text: string): boolean;
118
+ declare function measureContext(root: string, perFileWords?: number): Promise<ContextBudget>;
119
+
120
+ interface GeneratedFile {
121
+ path: string;
122
+ contents: string;
123
+ /** Present on disk already — apply will skip or ask before overwriting. */
124
+ exists: boolean;
125
+ /** The existing file's content differs from what the pack would generate (hand-edited or stale). */
126
+ differs?: boolean;
127
+ /** Hooks need the executable bit. */
128
+ executable?: boolean;
129
+ label: string;
130
+ }
131
+ interface PlanOptions {
132
+ /** Scaffold a minimal AGENTS.md (init only offers this when none exists). */
133
+ agents: boolean;
134
+ gates: boolean;
135
+ }
136
+ /** Build the file set an onboarding would write, flagging which exist and which differ. */
137
+ declare function planWorkflow(root: string, facts: ProjectFacts, opts: PlanOptions): Promise<GeneratedFile[]>;
138
+
139
+ interface ApplyResult {
140
+ written: string[];
141
+ skipped: string[];
142
+ }
143
+ /**
144
+ * Write a planned file set. Idempotent by default: an existing file is skipped unless `overwrite`
145
+ * names it — etymd never clobbers hand-authored contracts without consent.
146
+ */
147
+ declare function applyFiles(root: string, files: GeneratedFile[], overwrite?: Set<string>): Promise<ApplyResult>;
148
+
149
+ declare const CONFIG_FILE: string;
150
+ interface InstructionScope {
151
+ /** Extra instruction files to audit beyond the auto-detected set (globs, repo-relative). */
152
+ include: string[];
153
+ /** Instruction files to leave out of the audit (globs, repo-relative). */
154
+ exclude: string[];
155
+ }
156
+ interface ContextBudgets {
157
+ /** Words in one always-loaded file past which extraction is worth it. */
158
+ perFileWords: number;
159
+ /** Total always-loaded words past which the footprint itself is a finding. */
160
+ totalWords: number;
161
+ }
162
+ interface EtymdConfig {
163
+ instructions: InstructionScope;
164
+ context: ContextBudgets;
165
+ }
166
+ interface LoadedConfig {
167
+ config: EtymdConfig;
168
+ /** True when .etymd/config.json exists on disk (whether or not every key was usable). */
169
+ present: boolean;
170
+ /** Malformed or ignored input — surfaced as disclosures, never silently dropped. */
171
+ problems: string[];
172
+ }
173
+ declare const DEFAULT_CONFIG: EtymdConfig;
174
+ declare function configPath(root: string): string;
175
+ /**
176
+ * Load `.etymd/config.json`, falling back to defaults per key. Unreadable or malformed input is
177
+ * reported in `problems` rather than swallowed: config that silently fails to apply would let a
178
+ * repo believe it is scoping an audit that is in fact running unscoped (or vice versa).
179
+ */
180
+ declare function readConfig(root: string): Promise<LoadedConfig>;
181
+
182
+ type FindingTier = "risk" | "gap" | "polish";
183
+ type Effort = "S" | "M" | "L";
184
+ type Confidence = "high" | "medium" | "low";
185
+ interface Finding {
186
+ /** Stable across runs: `<lens>/<slug>` — the ledger keys on this. */
187
+ id: string;
188
+ lens: string;
189
+ tier: FindingTier;
190
+ /** One sentence: what is wrong / missing. */
191
+ claim: string;
192
+ /** File paths, job names, or metrics that ground the claim. Never empty. */
193
+ evidence: string[];
194
+ /** The concrete cost of NOT acting. */
195
+ why: string;
196
+ /** What to do about it (may name a etymd command as the fix). */
197
+ action?: string;
198
+ effort: Effort;
199
+ confidence: Confidence;
200
+ }
201
+ type LensKind = "truth" | "improvement";
202
+ type LensStatus = "ran" | "skipped";
203
+ interface LensReport {
204
+ lens: string;
205
+ version: string;
206
+ title: string;
207
+ kind: LensKind;
208
+ status: LensStatus;
209
+ /** Honest coverage: why the lens could not run, or what it could not see. */
210
+ reason?: string;
211
+ /** Partial-visibility disclosures (e.g. "3 CI jobs inherited from an unseen template"). */
212
+ disclosures: string[];
213
+ findings: Finding[];
214
+ /**
215
+ * Files this run deliberately did not examine (config exclusions). Their absence from
216
+ * `findings` means "unexamined", never "fixed" — the ledger must not resolve them.
217
+ */
218
+ outOfScope?: string[];
219
+ }
220
+ interface LensContext {
221
+ root: string;
222
+ facts: ProjectFacts;
223
+ profile: WorkflowProfile;
224
+ /** The committed, approved reckoning — what truth lenses measure drift against. */
225
+ baseline: Baseline | null;
226
+ /** The repo's optional `.etymd/config.json` (scope + budgets); defaults when absent. */
227
+ config?: LoadedConfig;
228
+ }
229
+ /** Solo vs team changes what counts as a gap (state docs and session archives are solo ritual). */
230
+ type WorkflowProfile = "solo" | "team";
231
+ interface Lens {
232
+ id: string;
233
+ version: string;
234
+ title: string;
235
+ kind: LensKind;
236
+ run(ctx: LensContext): Promise<LensReport>;
237
+ }
238
+ /** Canonical ranking: severity first, then cheapest wins inside a tier. */
239
+ declare function rankFindings(findings: Finding[]): Finding[];
240
+
241
+ interface Baseline {
242
+ packVersion: string;
243
+ etymdVersion: string;
244
+ approvedAt: string;
245
+ profile: WorkflowProfile;
246
+ facts: ProjectFacts;
247
+ }
248
+ declare function cacheFactsPath(root: string): string;
249
+ declare function baselinePath(root: string): string;
250
+ declare function writeCachedFacts(root: string, facts: ProjectFacts): Promise<string>;
251
+ declare function readCachedFacts(root: string): Promise<ProjectFacts | null>;
252
+ /**
253
+ * The scan's absolute root is a MACHINE path — it carries the author's username and directory
254
+ * layout. It must never reach the baseline, because the baseline is the one file etymd tells
255
+ * people to commit (and therefore to publish). It is redundant there anyway: the baseline lives
256
+ * inside the repo it describes, and `facts.name` already identifies the project.
257
+ *
258
+ * True for the gitignored cache too, but that one stays absolute on purpose — it never leaves the
259
+ * machine, and a wrong-checkout cache is easier to spot with the real path in it.
260
+ */
261
+ declare function withoutMachinePath(facts: ProjectFacts): ProjectFacts;
262
+ /** True for a baseline written before the root was elided — its holder should re-approve. */
263
+ declare function baselineCarriesMachinePath(baseline: Baseline): boolean;
264
+ declare function writeBaseline(root: string, baseline: Baseline): Promise<string>;
265
+ declare function readBaseline(root: string): Promise<Baseline | null>;
266
+ /** Solo vs team, from recent-author cardinality; init lets the human confirm/override. */
267
+ declare function deriveProfile(facts: ProjectFacts): WorkflowProfile;
268
+
269
+ type LedgerStatus = "open" | "accepted" | "done" | "dismissed" | "regressed";
270
+ interface LedgerEntry {
271
+ id: string;
272
+ status: LedgerStatus;
273
+ tier: Finding["tier"];
274
+ claim: string;
275
+ /** Only for dismissed — the human's reason, so the decision survives. */
276
+ reason?: string;
277
+ firstSeen: string;
278
+ lastSeen: string;
279
+ }
280
+ interface Ledger {
281
+ version: 1;
282
+ entries: LedgerEntry[];
283
+ }
284
+ interface LedgerDiff {
285
+ new: Finding[];
286
+ stillOpen: Finding[];
287
+ regressed: Finding[];
288
+ resolved: LedgerEntry[];
289
+ dismissed: Finding[];
290
+ accepted: Finding[];
291
+ /** Tracked findings in files this run excluded — held open, never counted as resolved. */
292
+ outOfScope: LedgerEntry[];
293
+ }
294
+ declare function readLedger(root: string): Promise<Ledger>;
295
+ declare function writeLedger(root: string, ledger: Ledger): Promise<void>;
296
+ /**
297
+ * Reconcile fresh findings against the ledger. Pure: returns the updated ledger + the diff;
298
+ * the caller decides whether to persist. A finding absent from a run is marked `done`
299
+ * (resolved); a `done` entry that reappears becomes `regressed`; `dismissed` stays dismissed.
300
+ *
301
+ * `outOfScope` names files the run deliberately did not look at (config exclusions). Their
302
+ * tracked findings are NOT absent because they were fixed — they are absent because nobody
303
+ * looked. Recording them as resolved would let scoping rewrite unfixed problems as successes,
304
+ * which is the same silence the exclusion disclosures exist to prevent. They are held untouched.
305
+ */
306
+ declare function reconcileLedger(ledger: Ledger, findings: Finding[], now?: string, outOfScope?: string[]): {
307
+ ledger: Ledger;
308
+ diff: LedgerDiff;
309
+ };
310
+
311
+ /** The lens registry — adding a lens means registering it here. */
312
+ declare const LENSES: Lens[];
313
+ interface AuditOptions {
314
+ /** Restrict to one kind (doctor = truth). */
315
+ kind?: LensKind;
316
+ /** Restrict to specific lens ids. */
317
+ lensIds?: string[];
318
+ /** Persist the reconciled ledger (the default; false = read-only report). */
319
+ persistLedger?: boolean;
320
+ }
321
+ interface AuditResult {
322
+ facts: ProjectFacts;
323
+ profile: WorkflowProfile;
324
+ baseline: Baseline | null;
325
+ config: LoadedConfig;
326
+ reports: LensReport[];
327
+ /** Ranked, dismissed-filtered — what the report shows. */
328
+ findings: Finding[];
329
+ ledger: Ledger;
330
+ diff: LedgerDiff;
331
+ }
332
+ declare function runAudit(root: string, opts?: AuditOptions): Promise<AuditResult>;
333
+
334
+ /**
335
+ * The truth lens: does what the instruction files CLAIM still hold against the actual repo?
336
+ * Commands must exist as scripts, paths must exist on disk, files must agree on the package
337
+ * manager, cross-references must resolve — plus drift against the committed baseline.
338
+ */
339
+ declare const instructionTruthLens: Lens;
340
+
341
+ interface InstructionFile {
342
+ /** Repo-relative path. */
343
+ path: string;
344
+ text: string;
345
+ }
346
+ interface InstructionFileSet {
347
+ /** The files the lens will actually audit. */
348
+ files: InstructionFile[];
349
+ /** Auto-detected files dropped by `instructions.exclude` — counted so scoping stays visible. */
350
+ excluded: string[];
351
+ /** Files pulled in by `instructions.include` that detection would have missed. */
352
+ included: string[];
353
+ }
354
+ /**
355
+ * Every agent-facing instruction file the scan knows how to find, then narrowed by the repo's
356
+ * optional scope: auto-detected ∪ `include`, minus `exclude`. The excluded set is returned rather
357
+ * than discarded — a scoped audit that quietly looked clean would be the exact dishonesty this
358
+ * tool exists to catch.
359
+ */
360
+ declare function listInstructionFiles(root: string, facts: ProjectFacts, scope?: InstructionScope): Promise<InstructionFileSet>;
361
+ interface CommandClaims {
362
+ /** Script names the file claims exist (deduped). */
363
+ scripts: Map<string, string>;
364
+ /** Workspace-filtered invocations we deliberately did not validate. */
365
+ filteredSkipped: number;
366
+ }
367
+ /** Script names referenced via `pnpm X` / `yarn X` / `npm run X` / `bun run X` / `npm test`. */
368
+ declare function extractCommandClaims(text: string): CommandClaims;
369
+ interface PathClaims {
370
+ /** Claims to verify against the repo. */
371
+ paths: string[];
372
+ /** Claims whose every mention sits in create-this prose — skipped, counted, disclosed. */
373
+ prospective: string[];
374
+ /** Naming stand-ins (`my-custom-skill`) — never real claims. */
375
+ placeholder: string[];
376
+ }
377
+ /**
378
+ * Repo-relative path claims from single-token inline spans, conservatively filtered. The
379
+ * load-bearing precision rule (learned from real corpus prose): an extensionless bare token
380
+ * (`research/trust`, `milestone/mNN`) is prose — a dir claim must end with `/`, a file claim
381
+ * must carry an extension.
382
+ */
383
+ declare function extractPathClaims(text: string): PathClaims;
384
+
385
+ /** Default total always-loaded words past which the footprint itself becomes a finding. */
386
+ declare const TOTAL_BUDGET_WORDS: number;
387
+ /**
388
+ * Economy lens: the always-loaded instruction footprint, as findings. Context is the dominant
389
+ * cost of the agent loop, and attention dilutes long before windows fill — a lean contract is
390
+ * a correctness feature, not a style preference.
391
+ */
392
+ declare const contextEconomyLens: Lens;
393
+
394
+ type GateTool = "typecheck" | "lint" | "format-check" | "format-write" | "test" | "coverage" | "e2e" | "sonar" | "codecov" | "commitlint" | "size" | "chromatic";
395
+ interface CiJobGate {
396
+ job: string;
397
+ file: string;
398
+ tools: GateTool[];
399
+ /** allow_failure / continue-on-error — an advisory job is NOT an enforced gate. */
400
+ advisory: boolean;
401
+ /** rules/only/except present — blocking-ness may differ per branch. */
402
+ branchScoped: boolean;
403
+ /** False when the job's script lives in an inherited template — tools are then inferred from
404
+ * the job name/variables only and may be incomplete. */
405
+ scriptVisible: boolean;
406
+ }
407
+ interface GateInventory {
408
+ local: {
409
+ source: ProjectFacts["hooks"]["source"];
410
+ wired: boolean;
411
+ preCommit: GateTool[];
412
+ prePush: GateTool[];
413
+ commitMsg: GateTool[];
414
+ lintStaged: GateTool[];
415
+ };
416
+ ci: {
417
+ system: ProjectFacts["ci"]["system"];
418
+ jobs: CiJobGate[];
419
+ /** include: entries pointing outside the repo — jobs we cannot see. */
420
+ inheritedIncludes: string[];
421
+ parseErrors: string[];
422
+ };
423
+ thresholds: {
424
+ sonarConfigured: boolean;
425
+ coverageThresholdLocal: boolean;
426
+ coverageCollected: boolean;
427
+ };
428
+ commitlintDep: boolean;
429
+ }
430
+ declare function buildGateInventory(root: string, facts: ProjectFacts): Promise<GateInventory>;
431
+
432
+ declare const gateIntegrityLens: Lens;
433
+
434
+ /**
435
+ * The knowledge-pack version — bumped whenever templates, the rubric, or the encoded rules
436
+ * change meaning. Stamped into facts, baselines, and generated artifacts so drift against the
437
+ * pack is computable and `harvest` has something to diff.
438
+ */
439
+ declare const PACK_VERSION = "2";
440
+
441
+ declare const VERSION: string;
442
+ declare const NAME: string;
443
+
444
+ export { type ApplyResult, type AuditOptions, type AuditResult, type Baseline, CONFIG_FILE, type ContextBudget, type ContextBudgets, type ContextFile, DEFAULT_CONFIG, type DetectedArtifact, type DiscoveredCommands, EXTRACTION_THRESHOLD, type EtymdConfig, type Finding, type GateInventory, type GateTool, type GeneratedFile, type HookFacts, type InstructionFileSet, type InstructionScope, LENSES, type Ledger, type LedgerDiff, type LedgerEntry, type Lens, type LensReport, type LoadedConfig, NAME, PACK_VERSION, type PackageManager, type PathClaims, type PlanOptions, type ProjectFacts, TOTAL_BUDGET_WORDS, VERSION, type WorkflowProfile, type WorkspaceKind, applyFiles, baselineCarriesMachinePath, baselinePath, buildGateInventory, cacheFactsPath, configPath, contextEconomyLens, deriveProfile, extractCommandClaims, extractPathClaims, gateIntegrityLens, instructionTruthLens, isAlwaysAppliedCursorRule, listInstructionFiles, measureContext, planWorkflow, rankFindings, readBaseline, readCachedFacts, readConfig, readLedger, reconcileLedger, runAudit, scanProject, withoutMachinePath, writeBaseline, writeCachedFacts, writeLedger };