etymd 0.2.0 → 0.2.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # etymd
2
2
 
3
+ ## 0.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - fdf6b6e: Fleet sweep: recurring classes + `placement: "none"` honored.
8
+
9
+ - **Recurring classes** — the sweep report groups open findings by their engine-minted
10
+ class prefix and lists every class present in ≥2 projects (worst tier first). A class
11
+ open in one repo is that repo's problem; the same class in four is a fleet lesson —
12
+ the report now asks "repo bug or fleet bug?" structurally, instead of relying on a
13
+ human to ask it at triage time. `--json` gains a `recurringClasses` array
14
+ (schema still EXPERIMENTAL through 0.2.x).
15
+ - **`placement: "none"` suppresses `no-contract`** — a registry entry declaring its
16
+ contract files legitimately absent no longer gets the absence re-reported every sweep.
17
+ The repo-local lens can't see the registry's declaration, so the fleet view honors it
18
+ (a standalone `etymd audit` in such a repo still reports it — correctly, since no
19
+ declaration is in scope there). Tier counts summarize the filtered list.
20
+
3
21
  ## 0.2.0
4
22
 
5
23
  ### Minor Changes
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { scanProject, PACK_VERSION } from './chunk-WNMA2FYM.js';
3
- import { VERSION } from './chunk-JQHGV74L.js';
4
- import { readBaseline, summarizeBaselineDrift, isDriftEmpty, writeBaseline, deriveProfile } from './chunk-DJ2DDFDK.js';
5
- import { section, theme, print, renderBaselineDrift, glyph } from './chunk-ASKJFMSX.js';
2
+ import { scanProject, PACK_VERSION } from './chunk-E2Q7WFPN.js';
3
+ import { VERSION } from './chunk-C4Z6NXDO.js';
4
+ import { readBaseline, summarizeBaselineDrift, isDriftEmpty, writeBaseline, deriveProfile } from './chunk-FZSIOCOZ.js';
5
+ import { section, theme, print, renderBaselineDrift, glyph } from './chunk-LBRNZZZF.js';
6
6
 
7
7
  // src/commands/approve.ts
8
8
  async function run(opts) {
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ export { run } from './chunk-6W3J4PJ6.js';
3
+ import './chunk-SS7VGLJ5.js';
4
+ import './chunk-KBF3SW3N.js';
5
+ import './chunk-ANCCGYSH.js';
6
+ import './chunk-E2Q7WFPN.js';
7
+ import './chunk-C4Z6NXDO.js';
8
+ import './chunk-FZSIOCOZ.js';
9
+ import './chunk-LBRNZZZF.js';
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { scanProject } from './chunk-WNMA2FYM.js';
3
- import './chunk-JQHGV74L.js';
4
- import { ETYMD_DIR } from './chunk-DJ2DDFDK.js';
5
- import { print, theme } from './chunk-ASKJFMSX.js';
2
+ import { scanProject } from './chunk-E2Q7WFPN.js';
3
+ import './chunk-C4Z6NXDO.js';
4
+ import { ETYMD_DIR } from './chunk-FZSIOCOZ.js';
5
+ import { print, theme } from './chunk-LBRNZZZF.js';
6
6
  import { promises } from 'node:fs';
7
7
  import path from 'node:path';
8
8
 
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { PACK_VERSION } from './chunk-WNMA2FYM.js';
3
- import { pathExists, readText } from './chunk-ASKJFMSX.js';
2
+ import { PACK_VERSION } from './chunk-E2Q7WFPN.js';
3
+ import { pathExists, readText } from './chunk-LBRNZZZF.js';
4
4
  import { promises } from 'node:fs';
5
5
  import path from 'node:path';
6
6
 
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { parseFailOnTier, runAudit, meetsFailOn } from './chunk-XGH2DTW4.js';
3
- import { print, section, theme, renderLensCoverage, renderFindings, renderLedgerDiff } from './chunk-ASKJFMSX.js';
2
+ import { parseFailOnTier, runAudit, meetsFailOn } from './chunk-SS7VGLJ5.js';
3
+ import { print, section, theme, renderLensCoverage, renderFindings, renderLedgerDiff } from './chunk-LBRNZZZF.js';
4
4
 
5
5
  // src/commands/audit.ts
6
6
  async function run(opts) {
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { ETYMD_DIR } from './chunk-DJ2DDFDK.js';
3
- import { pathExists, readText, wordCount, approxTokens } from './chunk-ASKJFMSX.js';
2
+ import { ETYMD_DIR } from './chunk-FZSIOCOZ.js';
3
+ import { pathExists, readText, wordCount, approxTokens } from './chunk-LBRNZZZF.js';
4
4
  import path2 from 'node:path';
5
5
  import { promises } from 'node:fs';
6
6
 
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // package.json
3
3
  var package_default = {
4
- version: "0.2.0"};
4
+ version: "0.2.1"};
5
5
 
6
6
  // src/version.ts
7
7
  var VERSION = package_default.version;
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { VERSION } from './chunk-JQHGV74L.js';
3
- import { normalizeRelPath, isDirectory, readJson, git, matchesAnyGlob, pathExists, readText } from './chunk-ASKJFMSX.js';
2
+ import { VERSION } from './chunk-C4Z6NXDO.js';
3
+ import { normalizeRelPath, isDirectory, readJson, git, matchesAnyGlob, pathExists, readText } from './chunk-LBRNZZZF.js';
4
4
  import path from 'node:path';
5
5
  import { promises } from 'node:fs';
6
6
 
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { readLedger, resolveEntry, writeLedger } from './chunk-XW5XQN23.js';
3
- import { print, renderLedger, glyph, theme } from './chunk-ASKJFMSX.js';
2
+ import { readLedger, resolveEntry, writeLedger } from './chunk-KBF3SW3N.js';
3
+ import { print, renderLedger, glyph, theme } from './chunk-LBRNZZZF.js';
4
4
 
5
5
  // src/commands/ledger.ts
6
6
  async function list(opts) {
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { readJson } from './chunk-ASKJFMSX.js';
2
+ import { readJson } from './chunk-LBRNZZZF.js';
3
3
  import { promises } from 'node:fs';
4
4
  import path from 'node:path';
5
5
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { readJson } from './chunk-ASKJFMSX.js';
2
+ import { readJson } from './chunk-LBRNZZZF.js';
3
3
  import { promises } from 'node:fs';
4
4
  import path from 'node:path';
5
5
 
@@ -356,4 +356,4 @@ function renderLedger(ledger) {
356
356
  }
357
357
  }
358
358
 
359
- export { approxTokens, git, glyph, isCiEnvironment, isDirectory, matchesAnyGlob, normalizeRelPath, pathExists, print, readJson, readText, renderBaselineDrift, renderContext, renderFacts, renderFindings, renderFleetNotes, renderFleetRows, renderLedger, renderLedgerDiff, renderLensCoverage, renderPlan, section, theme, wordCount };
359
+ export { TIER_BADGE, approxTokens, git, glyph, isCiEnvironment, isDirectory, matchesAnyGlob, normalizeRelPath, pathExists, print, readJson, readText, renderBaselineDrift, renderContext, renderFacts, renderFindings, renderFleetNotes, renderFleetRows, renderLedger, renderLedgerDiff, renderLensCoverage, renderPlan, section, theme, wordCount };
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { readLedger, reconcileLedger, writeLedger, visibleFindings } from './chunk-XW5XQN23.js';
3
- import { DEFAULT_CONFIG, readConfig, CONFIG_FILE, measureContext } from './chunk-3LBIBVX3.js';
4
- import { scanProject, PACK_VERSION, expandFileGlobs } from './chunk-WNMA2FYM.js';
5
- import { ETYMD_DIR, writeCachedFacts, readBaseline, deriveProfile, baselineCarriesMachinePath, BASELINE_FILE } from './chunk-DJ2DDFDK.js';
6
- import { pathExists, readJson, git, readText, isDirectory, matchesAnyGlob, isCiEnvironment, normalizeRelPath } from './chunk-ASKJFMSX.js';
2
+ import { readLedger, reconcileLedger, writeLedger, visibleFindings } from './chunk-KBF3SW3N.js';
3
+ import { DEFAULT_CONFIG, readConfig, CONFIG_FILE, measureContext } from './chunk-ANCCGYSH.js';
4
+ import { scanProject, PACK_VERSION, expandFileGlobs } from './chunk-E2Q7WFPN.js';
5
+ import { ETYMD_DIR, writeCachedFacts, readBaseline, deriveProfile, baselineCarriesMachinePath, BASELINE_FILE } from './chunk-FZSIOCOZ.js';
6
+ import { pathExists, readJson, git, readText, isDirectory, matchesAnyGlob, isCiEnvironment, normalizeRelPath } from './chunk-LBRNZZZF.js';
7
7
  import path from 'node:path';
8
8
  import YAML from 'yaml';
9
9
  import { promises } from 'node:fs';
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { VERSION } from './chunk-JQHGV74L.js';
2
+ import { VERSION } from './chunk-C4Z6NXDO.js';
3
3
  import path from 'node:path';
4
4
  import { Command } from 'commander';
5
5
  import pc from 'picocolors';
@@ -31,7 +31,7 @@ program.command("audit").description("Verify every instruction claim against the
31
31
  "exit non-zero when findings at/above this tier exist (risk|gap|polish)"
32
32
  ).action(
33
33
  (opts, cmd) => action(async () => {
34
- const { run } = await import('./audit-ATHB6FHN.js');
34
+ const { run } = await import('./audit-YH74YZFQ.js');
35
35
  await run({
36
36
  cwd: resolveCwd(cmd),
37
37
  json: opts.json,
@@ -44,7 +44,7 @@ program.command("audit").description("Verify every instruction claim against the
44
44
  );
45
45
  program.command("init").description("Onboard the truth guard: approve the baseline; scaffold AGENTS.md only if missing").option("-y, --yes", "accept defaults without prompting (never overwrites)").action(
46
46
  (opts, cmd) => action(async () => {
47
- const { run } = await import('./init-WLYWEWRQ.js');
47
+ const { run } = await import('./init-ISEFZWPV.js');
48
48
  await run({ cwd: resolveCwd(cmd), yes: opts.yes });
49
49
  })
50
50
  );
@@ -52,49 +52,49 @@ program.command("approve").description(
52
52
  "Re-approve the committed baseline after intentional structural changes (non-interactive)"
53
53
  ).action(
54
54
  (_opts, cmd) => action(async () => {
55
- const { run } = await import('./approve-EJJDCNYV.js');
55
+ const { run } = await import('./approve-X3AW7KLD.js');
56
56
  await run({ cwd: resolveCwd(cmd) });
57
57
  })
58
58
  );
59
59
  program.command("scan").description("Deterministically reckon the project into a facts index").option("--json", "print the raw facts as JSON").option("--no-save", "do not write the .etymd cache").action(
60
60
  (opts, cmd) => action(async () => {
61
- const { run } = await import('./scan-62KGD2V3.js');
61
+ const { run } = await import('./scan-ADZ2RKYY.js');
62
62
  await run({ cwd: resolveCwd(cmd), json: opts.json, save: opts.save });
63
63
  })
64
64
  );
65
65
  program.command("doctor").description('Alias for `audit --truth` \u2014 "are the recorded instructions still true?"').option("--json", "print findings as JSON").action(
66
66
  (opts, cmd) => action(async () => {
67
- const { run } = await import('./doctor-WTYZSJ5X.js');
67
+ const { run } = await import('./doctor-EAA4IP4Y.js');
68
68
  await run({ cwd: resolveCwd(cmd), json: opts.json });
69
69
  })
70
70
  );
71
71
  program.command("context").description("Measure the always-loaded context footprint and flag extraction candidates").option("--json", "print the budget as JSON").action(
72
72
  (opts, cmd) => action(async () => {
73
- const { run } = await import('./context-5HAWI2RD.js');
73
+ const { run } = await import('./context-GYCLR3TG.js');
74
74
  await run({ cwd: resolveCwd(cmd), json: opts.json });
75
75
  })
76
76
  );
77
77
  program.command("brief").description("Emit a grounded briefing for the in-repo agent to complete the semantic layer").option("--human", "write a human onboarding brief instead of the agent briefing").action(
78
78
  (opts, cmd) => action(async () => {
79
- const { run } = await import('./brief-GFR3YOZT.js');
79
+ const { run } = await import('./brief-RT6KE4CN.js');
80
80
  await run({ cwd: resolveCwd(cmd), human: opts.human });
81
81
  })
82
82
  );
83
83
  program.command("ledger").description("List every tracked finding grouped by status (open \xB7 accepted \xB7 dismissed \xB7 \u2026)").option("--json", "print the raw ledger as JSON").action(
84
84
  (opts, cmd) => action(async () => {
85
- const { list } = await import('./ledger-ENFHLNOG.js');
85
+ const { list } = await import('./ledger-RHQJH5EA.js');
86
86
  await list({ cwd: resolveCwd(cmd), json: opts.json });
87
87
  })
88
88
  );
89
89
  program.command("dismiss").argument("<finding-id>", "the finding id (from `etymd audit`) to dismiss").description("Dismiss a finding as a false positive / not applicable (hidden from future audits)").requiredOption("--reason <text>", "why it is dismissed \u2014 recorded so the decision survives").action(
90
90
  (id, opts, cmd) => action(async () => {
91
- const { dismiss } = await import('./ledger-ENFHLNOG.js');
91
+ const { dismiss } = await import('./ledger-RHQJH5EA.js');
92
92
  await dismiss({ cwd: resolveCwd(cmd), id, reason: opts.reason });
93
93
  })
94
94
  );
95
95
  program.command("accept").argument("<finding-id>", "the finding id (from `etymd audit`) to accept").description("Accept a real finding as a known trade-off (hidden from future audits)").option("--reason <text>", "optional note on why the trade-off is accepted").action(
96
96
  (id, opts, cmd) => action(async () => {
97
- const { accept } = await import('./ledger-ENFHLNOG.js');
97
+ const { accept } = await import('./ledger-RHQJH5EA.js');
98
98
  await accept({ cwd: resolveCwd(cmd), id, reason: opts.reason });
99
99
  })
100
100
  );
@@ -111,7 +111,7 @@ var fleet = program.command("fleet").description(
111
111
  "exit non-zero when findings at/above this tier exist (risk|gap|polish)"
112
112
  ).action(
113
113
  (opts, cmd) => action(async () => {
114
- const { sweep } = await import('./fleet-2GXLNLJE.js');
114
+ const { sweep } = await import('./fleet-IS6YWDDV.js');
115
115
  await sweep({
116
116
  cwd: resolveCwd(cmd),
117
117
  manifest: opts.manifest,
@@ -126,7 +126,7 @@ var fleet = program.command("fleet").description(
126
126
  );
127
127
  fleet.command("check").description("Validate the manifest pair only \u2014 no lenses: dangling mappings, duplicates, leaks").option("--manifest <file>", "the fleet manifest \u2014 required unless the cwd holds registry.json").option("--json", "print the findings as JSON (EXPERIMENTAL through 0.2.x)").action(
128
128
  (_opts, cmd) => action(async () => {
129
- const { check } = await import('./fleet-2GXLNLJE.js');
129
+ const { check } = await import('./fleet-IS6YWDDV.js');
130
130
  const opts = cmd.optsWithGlobals();
131
131
  await check({ cwd: resolveCwd(cmd), manifest: opts.manifest, json: opts.json });
132
132
  })
@@ -135,7 +135,7 @@ fleet.command("dismiss").argument("<name>", "the registered project name").argum
135
135
  "Dismiss a project's finding from any cwd \u2014 corp ledgers persist beside the manifest"
136
136
  ).requiredOption("--reason <text>", "why it is dismissed \u2014 recorded so the decision survives").option("--manifest <file>", "the fleet manifest \u2014 required unless the cwd holds registry.json").action(
137
137
  (name, id, _opts, cmd) => action(async () => {
138
- const { dismiss } = await import('./fleet-2GXLNLJE.js');
138
+ const { dismiss } = await import('./fleet-IS6YWDDV.js');
139
139
  const opts = cmd.optsWithGlobals();
140
140
  await dismiss({
141
141
  cwd: resolveCwd(cmd),
@@ -150,14 +150,14 @@ fleet.command("accept").argument("<name>", "the registered project name").argume
150
150
  "Accept a project's finding as a known trade-off \u2014 corp ledgers persist beside the manifest"
151
151
  ).option("--reason <text>", "optional note on why the trade-off is accepted").option("--manifest <file>", "the fleet manifest \u2014 required unless the cwd holds registry.json").action(
152
152
  (name, id, _opts, cmd) => action(async () => {
153
- const { accept } = await import('./fleet-2GXLNLJE.js');
153
+ const { accept } = await import('./fleet-IS6YWDDV.js');
154
154
  const opts = cmd.optsWithGlobals();
155
155
  await accept({ cwd: resolveCwd(cmd), manifest: opts.manifest, name, id, reason: opts.reason });
156
156
  })
157
157
  );
158
158
  program.command("gates").description("Install the local git-hook gates (process \u2192 pre-commit, correctness \u2192 pre-push)").option("--ci", "note about the CI review gate (ships later; local gates install now)").option("-y, --yes", "skip prompts; never overwrites a hand-edited hook").action(
159
159
  (opts, cmd) => action(async () => {
160
- const { run } = await import('./gates-LD276XN5.js');
160
+ const { run } = await import('./gates-3NPKF4IZ.js');
161
161
  await run({ cwd: resolveCwd(cmd), ci: opts.ci, yes: opts.yes });
162
162
  })
163
163
  );
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { readConfig, measureContext } from './chunk-3LBIBVX3.js';
3
- import './chunk-DJ2DDFDK.js';
4
- import { print, renderContext } from './chunk-ASKJFMSX.js';
2
+ import { readConfig, measureContext } from './chunk-ANCCGYSH.js';
3
+ import './chunk-FZSIOCOZ.js';
4
+ import { print, renderContext } from './chunk-LBRNZZZF.js';
5
5
 
6
6
  // src/commands/context.ts
7
7
  async function run(opts) {
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ import { run } from './chunk-6W3J4PJ6.js';
3
+ import './chunk-SS7VGLJ5.js';
4
+ import './chunk-KBF3SW3N.js';
5
+ import './chunk-ANCCGYSH.js';
6
+ import './chunk-E2Q7WFPN.js';
7
+ import './chunk-C4Z6NXDO.js';
8
+ import './chunk-FZSIOCOZ.js';
9
+ import './chunk-LBRNZZZF.js';
10
+
11
+ // src/commands/doctor.ts
12
+ async function run2(opts) {
13
+ await run({ cwd: opts.cwd, json: opts.json, truth: true });
14
+ }
15
+
16
+ export { run2 as run };
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { dismiss, accept } from './chunk-B4FYQE7S.js';
3
- import { parseFailOnTier, meetsFailOn, runAudit } from './chunk-XGH2DTW4.js';
4
- import { readLedger } from './chunk-XW5XQN23.js';
5
- import { DEFAULT_CONFIG } from './chunk-3LBIBVX3.js';
6
- import './chunk-WNMA2FYM.js';
7
- import './chunk-JQHGV74L.js';
8
- import { ETYMD_DIR } from './chunk-DJ2DDFDK.js';
9
- import { print, theme, section, renderFleetRows, renderFleetNotes, renderFindings, glyph, pathExists, readText, isDirectory, git } from './chunk-ASKJFMSX.js';
2
+ import { dismiss, accept } from './chunk-EUIHYD6F.js';
3
+ import { parseFailOnTier, meetsFailOn, runAudit } from './chunk-SS7VGLJ5.js';
4
+ import { readLedger } from './chunk-KBF3SW3N.js';
5
+ import { DEFAULT_CONFIG } from './chunk-ANCCGYSH.js';
6
+ import './chunk-E2Q7WFPN.js';
7
+ import './chunk-C4Z6NXDO.js';
8
+ import { ETYMD_DIR } from './chunk-FZSIOCOZ.js';
9
+ import { print, theme, section, renderFleetRows, renderFleetNotes, renderFindings, TIER_BADGE, glyph, pathExists, readText, isDirectory, git } from './chunk-LBRNZZZF.js';
10
10
  import { promises } from 'node:fs';
11
11
  import path from 'node:path';
12
12
  import os from 'node:os';
@@ -246,6 +246,25 @@ var pExecFile = promisify(execFile);
246
246
  var FLEET_LENS = "fleet-manifest";
247
247
  var FLEET_JSON_SCHEMA = "fleet-experimental-0.2";
248
248
  var CORP_WALL_ARTIFACTS = ["PROJECT_CONTEXT.md", "DECISIONS.md"];
249
+ function recurringClasses(projects) {
250
+ const byClass = /* @__PURE__ */ new Map();
251
+ for (const p of projects) {
252
+ for (const f of p.findings) {
253
+ const classId = f.id.split(":")[0] ?? f.id;
254
+ const entry = byClass.get(classId);
255
+ if (entry) {
256
+ entry.projects.add(p.name);
257
+ if (TIER_ORDER.indexOf(f.tier) < TIER_ORDER.indexOf(entry.tier)) entry.tier = f.tier;
258
+ } else {
259
+ byClass.set(classId, { tier: f.tier, projects: /* @__PURE__ */ new Set([p.name]) });
260
+ }
261
+ }
262
+ }
263
+ return [...byClass.entries()].filter(([, v]) => v.projects.size >= 2).map(([classId, v]) => ({ classId, tier: v.tier, projects: [...v.projects].sort() })).sort(
264
+ (a, b) => TIER_ORDER.indexOf(a.tier) - TIER_ORDER.indexOf(b.tier) || b.projects.length - a.projects.length || a.classId.localeCompare(b.classId)
265
+ );
266
+ }
267
+ var TIER_ORDER = ["risk", "gap", "polish"];
249
268
  function finding(id, tier, claim, evidence, why, action) {
250
269
  return {
251
270
  id,
@@ -695,7 +714,10 @@ async function sweepEntry(manifest, entry, opts) {
695
714
  upstreamRemote
696
715
  });
697
716
  project.findings = result.findings;
698
- for (const f of result.findings) project.counts[f.tier] += 1;
717
+ if (entry.contract.placement === "none") {
718
+ project.findings = project.findings.filter((f) => !f.id.endsWith("/no-contract"));
719
+ }
720
+ for (const f of project.findings) project.counts[f.tier] += 1;
699
721
  project.stateAgeDays = stateAgeDays(result.facts);
700
722
  project.staleAfterDays = result.config.config.state.staleAfterDays;
701
723
  for (const [key, value] of Object.entries(entry.contract)) {
@@ -737,7 +759,8 @@ async function sweepFleet(manifest, opts = {}) {
737
759
  wall: wall.findings,
738
760
  wallDisclosures: wall.disclosures,
739
761
  outOfScope: projects.filter((p) => !p.resolvedRoot || p.unresolved).map((p) => p.name),
740
- problems: manifest.problems.map((p) => `${p.file}: ${p.detail}`)
762
+ problems: manifest.problems.map((p) => `${p.file}: ${p.detail}`),
763
+ recurringClasses: recurringClasses(projects)
741
764
  };
742
765
  }
743
766
  async function fleetLedgerTarget(manifest, name) {
@@ -906,6 +929,14 @@ function renderSweep(result, previous, delta, wallNew) {
906
929
  section(`New or risk findings ${theme.dim(`(${detail.length})`)}`);
907
930
  renderFindings(detail);
908
931
  }
932
+ if (result.recurringClasses.length) {
933
+ section(`Recurring classes ${theme.dim("(open in \u22652 projects \u2014 class-fix candidates)")}`);
934
+ for (const rc of result.recurringClasses) {
935
+ print(
936
+ ` ${TIER_BADGE[rc.tier]} ${rc.classId} ${theme.dim(`\u2014 ${rc.projects.length}\xD7: ${rc.projects.join(", ")}`)}`
937
+ );
938
+ }
939
+ }
909
940
  section("Fleet wall");
910
941
  if (!result.wall.length) print(` ${glyph.ok} ${theme.dim("clean")}`);
911
942
  else
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { planWorkflow, applyFiles } from './chunk-WQXMJUIA.js';
3
- import { scanProject } from './chunk-WNMA2FYM.js';
4
- import './chunk-JQHGV74L.js';
5
- import { section, print, glyph, theme, renderPlan, git } from './chunk-ASKJFMSX.js';
2
+ import { planWorkflow, applyFiles } from './chunk-4UZBCCFJ.js';
3
+ import { scanProject } from './chunk-E2Q7WFPN.js';
4
+ import './chunk-C4Z6NXDO.js';
5
+ import { section, print, glyph, theme, renderPlan, git } from './chunk-LBRNZZZF.js';
6
6
  import { confirm, isCancel, cancel } from '@clack/prompts';
7
7
 
8
8
  async function run(opts) {
package/dist/index.d.ts CHANGED
@@ -499,6 +499,15 @@ interface FleetSweepResult {
499
499
  outOfScope: string[];
500
500
  /** Manifest problems, verbatim — disclosed by every renderer, never defaulted away. */
501
501
  problems: string[];
502
+ /** Defect classes open in ≥2 projects — the class-fix candidates (see recurringClasses()). */
503
+ recurringClasses: RecurringClass[];
504
+ }
505
+ interface RecurringClass {
506
+ /** The `<lens>/<class>` id prefix — minted by the engine's lenses, identical across repos. */
507
+ classId: string;
508
+ tier: FindingTier;
509
+ /** Project names (corp entries by alias) where the class is currently open. */
510
+ projects: string[];
502
511
  }
503
512
  /**
504
513
  * Where a corp entry's audit state persists: beside the manifest, never in the worktree.
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@ var PACK_VERSION = "2";
13
13
  // package.json
14
14
  var package_default = {
15
15
  name: "etymd",
16
- version: "0.2.0"};
16
+ version: "0.2.1"};
17
17
 
18
18
  // src/version.ts
19
19
  var VERSION = package_default.version;
@@ -2622,6 +2622,25 @@ var pExecFile2 = promisify(execFile);
2622
2622
  var FLEET_LENS = "fleet-manifest";
2623
2623
  var FLEET_JSON_SCHEMA = "fleet-experimental-0.2";
2624
2624
  var CORP_WALL_ARTIFACTS = ["PROJECT_CONTEXT.md", "DECISIONS.md"];
2625
+ function recurringClasses(projects) {
2626
+ const byClass = /* @__PURE__ */ new Map();
2627
+ for (const p of projects) {
2628
+ for (const f of p.findings) {
2629
+ const classId = f.id.split(":")[0] ?? f.id;
2630
+ const entry = byClass.get(classId);
2631
+ if (entry) {
2632
+ entry.projects.add(p.name);
2633
+ if (TIER_ORDER2.indexOf(f.tier) < TIER_ORDER2.indexOf(entry.tier)) entry.tier = f.tier;
2634
+ } else {
2635
+ byClass.set(classId, { tier: f.tier, projects: /* @__PURE__ */ new Set([p.name]) });
2636
+ }
2637
+ }
2638
+ }
2639
+ return [...byClass.entries()].filter(([, v]) => v.projects.size >= 2).map(([classId, v]) => ({ classId, tier: v.tier, projects: [...v.projects].sort() })).sort(
2640
+ (a, b) => TIER_ORDER2.indexOf(a.tier) - TIER_ORDER2.indexOf(b.tier) || b.projects.length - a.projects.length || a.classId.localeCompare(b.classId)
2641
+ );
2642
+ }
2643
+ var TIER_ORDER2 = ["risk", "gap", "polish"];
2625
2644
  function finding3(id, tier, claim, evidence, why, action) {
2626
2645
  return {
2627
2646
  id,
@@ -3071,7 +3090,10 @@ async function sweepEntry(manifest, entry, opts) {
3071
3090
  upstreamRemote
3072
3091
  });
3073
3092
  project.findings = result.findings;
3074
- for (const f of result.findings) project.counts[f.tier] += 1;
3093
+ if (entry.contract.placement === "none") {
3094
+ project.findings = project.findings.filter((f) => !f.id.endsWith("/no-contract"));
3095
+ }
3096
+ for (const f of project.findings) project.counts[f.tier] += 1;
3075
3097
  project.stateAgeDays = stateAgeDays(result.facts);
3076
3098
  project.staleAfterDays = result.config.config.state.staleAfterDays;
3077
3099
  for (const [key, value] of Object.entries(entry.contract)) {
@@ -3113,7 +3135,8 @@ async function sweepFleet(manifest, opts = {}) {
3113
3135
  wall: wall.findings,
3114
3136
  wallDisclosures: wall.disclosures,
3115
3137
  outOfScope: projects.filter((p) => !p.resolvedRoot || p.unresolved).map((p) => p.name),
3116
- problems: manifest.problems.map((p) => `${p.file}: ${p.detail}`)
3138
+ problems: manifest.problems.map((p) => `${p.file}: ${p.detail}`),
3139
+ recurringClasses: recurringClasses(projects)
3117
3140
  };
3118
3141
  }
3119
3142
 
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { planWorkflow, applyFiles } from './chunk-WQXMJUIA.js';
3
- import { scanProject, PACK_VERSION } from './chunk-WNMA2FYM.js';
4
- import { VERSION } from './chunk-JQHGV74L.js';
5
- import { writeCachedFacts, deriveProfile, writeBaseline, CACHE_DIR } from './chunk-DJ2DDFDK.js';
6
- import { theme, renderFacts, print, renderPlan, git, section, glyph, readText } from './chunk-ASKJFMSX.js';
2
+ import { planWorkflow, applyFiles } from './chunk-4UZBCCFJ.js';
3
+ import { scanProject, PACK_VERSION } from './chunk-E2Q7WFPN.js';
4
+ import { VERSION } from './chunk-C4Z6NXDO.js';
5
+ import { writeCachedFacts, deriveProfile, writeBaseline, CACHE_DIR } from './chunk-FZSIOCOZ.js';
6
+ import { theme, renderFacts, print, renderPlan, git, section, glyph, readText } from './chunk-LBRNZZZF.js';
7
7
  import { promises } from 'node:fs';
8
8
  import path from 'node:path';
9
9
  import { intro, spinner, select, confirm, outro, isCancel, cancel } from '@clack/prompts';
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ export { accept, dismiss, list } from './chunk-EUIHYD6F.js';
3
+ import './chunk-KBF3SW3N.js';
4
+ import './chunk-LBRNZZZF.js';
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { scanProject } from './chunk-WNMA2FYM.js';
3
- import './chunk-JQHGV74L.js';
4
- import { writeCachedFacts } from './chunk-DJ2DDFDK.js';
5
- import { print, renderFacts, theme } from './chunk-ASKJFMSX.js';
2
+ import { scanProject } from './chunk-E2Q7WFPN.js';
3
+ import './chunk-C4Z6NXDO.js';
4
+ import { writeCachedFacts } from './chunk-FZSIOCOZ.js';
5
+ import { print, renderFacts, theme } from './chunk-LBRNZZZF.js';
6
6
 
7
7
  // src/commands/scan.ts
8
8
  async function run(opts) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "etymd",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Keep your agent instructions true — verify AGENTS.md, CLAUDE.md, rules & skills against the actual repo, with drift caught over time and a regression ledger.",
5
5
  "keywords": [
6
6
  "cli",
@@ -1,9 +0,0 @@
1
- #!/usr/bin/env node
2
- export { run } from './chunk-3WOORFOW.js';
3
- import './chunk-XGH2DTW4.js';
4
- import './chunk-XW5XQN23.js';
5
- import './chunk-3LBIBVX3.js';
6
- import './chunk-WNMA2FYM.js';
7
- import './chunk-JQHGV74L.js';
8
- import './chunk-DJ2DDFDK.js';
9
- import './chunk-ASKJFMSX.js';
@@ -1,16 +0,0 @@
1
- #!/usr/bin/env node
2
- import { run } from './chunk-3WOORFOW.js';
3
- import './chunk-XGH2DTW4.js';
4
- import './chunk-XW5XQN23.js';
5
- import './chunk-3LBIBVX3.js';
6
- import './chunk-WNMA2FYM.js';
7
- import './chunk-JQHGV74L.js';
8
- import './chunk-DJ2DDFDK.js';
9
- import './chunk-ASKJFMSX.js';
10
-
11
- // src/commands/doctor.ts
12
- async function run2(opts) {
13
- await run({ cwd: opts.cwd, json: opts.json, truth: true });
14
- }
15
-
16
- export { run2 as run };
@@ -1,4 +0,0 @@
1
- #!/usr/bin/env node
2
- export { accept, dismiss, list } from './chunk-B4FYQE7S.js';
3
- import './chunk-XW5XQN23.js';
4
- import './chunk-ASKJFMSX.js';