etymd 0.3.0 → 0.3.2

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,54 @@
1
1
  # etymd
2
2
 
3
+ ## 0.3.2
4
+
5
+ ### Patch Changes
6
+
7
+ - A truth finding from any lens now reaches a `doctor` run.
8
+
9
+ `doctor` filters to truth-only, and it used to filter at the **lens** level — so `gate-integrity`
10
+ (an improvement lens) was skipped entirely, and its `hooks-not-wired` finding (tier: risk —
11
+ "tracked hooks exist but never run") was invisible in the one view meant to catch it. The lens
12
+ label was never the right granularity: a lens can emit both kinds, and "hooks exist and are
13
+ unwired" is as objective as anything the truth lens reports.
14
+
15
+ `kind` now lives on the Finding, defaulting to the lens kind when a finding does not set its
16
+ own, and the `--truth` filter operates at the finding level. Every lens still runs under
17
+ `--truth`; findings are filtered afterward, so nothing objective is invisible.
18
+
19
+ Also in this release: CI proves the publish tarball's exact manifest on every push (packagers
20
+ ignore `.gitignore`, so a stray local file can ship while every git-scoped check stays green),
21
+ and the generated hook companions (`.githooks/<hook>.local`) ship in this repo as a worked
22
+ example of the resolve-by-name convention.
23
+
24
+ ## 0.3.1
25
+
26
+ ### Patch Changes
27
+
28
+ - Regeneration keeps what a repo already had, and a fleet can declare a repo has no gates.
29
+
30
+ **Preservation moved into the generator.** `etymd gates` read the existing hook and kept a test
31
+ step it already ran; every other caller of `planWorkflow` did not — including the fleet gate-drift
32
+ check, which therefore compared each repo against a hook missing checks the repo really runs.
33
+ That reported permanent false drift, and anyone acting on it would have silently lost a check. A
34
+ guarantee that holds only on the path someone remembered to wire it into is not a guarantee.
35
+
36
+ **`gates: "none"` in a fleet manifest** declares that generated gates are deliberately absent — a
37
+ prose repo with nothing mechanically checkable, or one that gates itself by hand. Gate drift sits
38
+ among the wall checks, which are deliberately not ledger-quietable because a leak or a partition
39
+ breach has no honest resolution except a fix; that reasoning does not transfer to a settled
40
+ choice, and an un-silenceable finding for a decision already made is how a report teaches people
41
+ to stop reading it. The absence is disclosed rather than hidden, and an _undeclared_ absence still
42
+ reports — silence has to be earned by declaring it.
43
+
44
+ **A hand-written `.etymd/config.json` no longer crashes the generator.** Setting only
45
+ `gates.failOn` threw: the type declares `commands` as required, describing the shape etymd writes
46
+ rather than the shape a user writes.
47
+
48
+ **The publish gate points at the committed script.** `prepublishOnly` resolving a bare
49
+ `artifact-check` from `PATH` meant the door guarding what actually ships was silently absent for
50
+ every clone but one.
51
+
3
52
  ## 0.3.0
4
53
 
5
54
  ### Minor Changes
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { section, theme, print, renderBaselineDrift, glyph } from './chunk-TADKOW6P.js';
3
+ import { scanProject } from './chunk-7MOBQ4N5.js';
4
+ import { VERSION } from './chunk-B3SUTZVY.js';
3
5
  import { readBaseline, summarizeBaselineDrift, isDriftEmpty, writeBaseline, deriveProfile } from './chunk-R74SJ7HS.js';
4
- import { scanProject } from './chunk-MKYPLNLH.js';
5
- import { VERSION } from './chunk-NFENQ3IK.js';
6
6
  import { PACK_VERSION } from './chunk-CRJHIXJH.js';
7
7
  import './chunk-YLNQQZ5F.js';
8
8
 
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- export { run } from './chunk-XTAX4YNT.js';
3
- import './chunk-V3DPLOOM.js';
2
+ export { run } from './chunk-WOBHQQ6H.js';
3
+ import './chunk-WUGBAW6Z.js';
4
4
  import './chunk-LT67FU2Y.js';
5
5
  import './chunk-WWBF4Y7K.js';
6
6
  import './chunk-NMZ3RHWW.js';
7
7
  import './chunk-TADKOW6P.js';
8
+ import './chunk-7MOBQ4N5.js';
9
+ import './chunk-B3SUTZVY.js';
8
10
  import './chunk-R74SJ7HS.js';
9
- import './chunk-MKYPLNLH.js';
10
- import './chunk-NFENQ3IK.js';
11
11
  import './chunk-CRJHIXJH.js';
12
12
  import './chunk-YLNQQZ5F.js';
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { print, theme } from './chunk-TADKOW6P.js';
3
+ import { scanProject } from './chunk-7MOBQ4N5.js';
4
+ import './chunk-B3SUTZVY.js';
3
5
  import { ETYMD_DIR } from './chunk-R74SJ7HS.js';
4
- import { scanProject } from './chunk-MKYPLNLH.js';
5
- import './chunk-NFENQ3IK.js';
6
6
  import './chunk-CRJHIXJH.js';
7
7
  import './chunk-YLNQQZ5F.js';
8
8
  import { promises } from 'node:fs';
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { VERSION } from './chunk-NFENQ3IK.js';
2
+ import { VERSION } from './chunk-B3SUTZVY.js';
3
3
  import { PACK_VERSION } from './chunk-CRJHIXJH.js';
4
4
  import { normalizeRelPath, isDirectory, readJson, git, matchesAnyGlob, pathExists, readText } from './chunk-YLNQQZ5F.js';
5
5
  import path from 'node:path';
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // package.json
3
3
  var package_default = {
4
- version: "0.3.0"};
4
+ version: "0.3.2"};
5
5
 
6
6
  // src/version.ts
7
7
  var VERSION = package_default.version;
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import { DEFAULT_CONFIG } from './chunk-NMZ3RHWW.js';
2
3
  import { PACK_VERSION } from './chunk-CRJHIXJH.js';
3
- import { pathExists, readText } from './chunk-YLNQQZ5F.js';
4
+ import { readText, pathExists } from './chunk-YLNQQZ5F.js';
4
5
  import path from 'node:path';
5
6
 
6
7
  // src/pack/templates.ts
@@ -160,7 +161,7 @@ exit 0
160
161
  function generatePrePushHook(facts, gates) {
161
162
  const run = runPrefix(facts.packageManager);
162
163
  const c = facts.commands;
163
- const candidates = gates?.commands.length ? gates.commands : [c.formatCheck, c.typecheck, c.lint];
164
+ const candidates = gates?.commands?.length ? gates.commands : [c.formatCheck, c.typecheck, c.lint];
164
165
  const allowed = new Set(gates?.allowWriting ?? []);
165
166
  const steps = candidates.filter(
166
167
  (key) => Boolean(key) && (allowed.has(key) || isSafeGateCommand(c.raw[key]))
@@ -227,6 +228,16 @@ exit 0
227
228
  }
228
229
 
229
230
  // src/core/generate.ts
231
+ function derivedCommands(facts, existingHook) {
232
+ const c = facts.commands;
233
+ const base = [c.formatCheck, c.typecheck, c.lint].filter(
234
+ (k) => Boolean(k) && isSafeGateCommand(c.raw[k])
235
+ );
236
+ if (c.test && existingHook && new RegExp(`\\b${c.test}\\b`).test(existingHook)) {
237
+ base.push(c.test);
238
+ }
239
+ return base;
240
+ }
230
241
  async function planWorkflow(root, facts, opts) {
231
242
  const out = [];
232
243
  const add = async (rel, contents, label, executable = false) => {
@@ -246,6 +257,7 @@ async function planWorkflow(root, facts, opts) {
246
257
  await add("AGENTS.md", generateAgentsMd(facts), "Minimal operating contract (scaffold)");
247
258
  }
248
259
  if (opts.gates) {
260
+ const existingPrePush = await readText(path.join(root, ".githooks", "pre-push"));
249
261
  await add(".githooks/pre-commit", generatePreCommitHook(), "Process gate (pre-commit)", true);
250
262
  await add(
251
263
  ".githooks/commit-msg",
@@ -253,9 +265,15 @@ async function planWorkflow(root, facts, opts) {
253
265
  "Content screen (commit message)",
254
266
  true
255
267
  );
268
+ const gateConfig = opts.gateConfig?.commands?.length ? opts.gateConfig : {
269
+ commands: derivedCommands(facts, existingPrePush ?? void 0),
270
+ failOn: opts.gateConfig?.failOn ?? DEFAULT_CONFIG.gates.failOn,
271
+ publishGate: opts.gateConfig?.publishGate,
272
+ allowWriting: opts.gateConfig?.allowWriting ?? []
273
+ };
256
274
  await add(
257
275
  ".githooks/pre-push",
258
- generatePrePushHook(facts, opts.gateConfig),
276
+ generatePrePushHook(facts, gateConfig),
259
277
  "Correctness gate (pre-push)",
260
278
  true
261
279
  );
@@ -271,4 +289,4 @@ async function planWorkflow(root, facts, opts) {
271
289
  return out;
272
290
  }
273
291
 
274
- export { isSafeGateCommand, planWorkflow, runPrefix };
292
+ export { derivedCommands, isSafeGateCommand, planWorkflow, runPrefix };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { parseFailOnTier, runAudit, meetsFailOn } from './chunk-V3DPLOOM.js';
2
+ import { parseFailOnTier, runAudit, meetsFailOn } from './chunk-WUGBAW6Z.js';
3
3
  import { print, section, theme, renderLensCoverage, renderFindings, renderLedgerDiff } from './chunk-TADKOW6P.js';
4
4
 
5
5
  // src/commands/audit.ts
@@ -2,8 +2,8 @@
2
2
  import { measureContext } from './chunk-LT67FU2Y.js';
3
3
  import { readLedger, reconcileLedger, writeLedger, visibleFindings } from './chunk-WWBF4Y7K.js';
4
4
  import { DEFAULT_CONFIG, readConfig, CONFIG_FILE } from './chunk-NMZ3RHWW.js';
5
+ import { scanProject, expandFileGlobs } from './chunk-7MOBQ4N5.js';
5
6
  import { ETYMD_DIR, writeCachedFacts, readBaseline, deriveProfile, baselineCarriesMachinePath, BASELINE_FILE } from './chunk-R74SJ7HS.js';
6
- import { scanProject, expandFileGlobs } from './chunk-MKYPLNLH.js';
7
7
  import { PACK_VERSION } from './chunk-CRJHIXJH.js';
8
8
  import { pathExists, readJson, git, readText, isDirectory, matchesAnyGlob, isCiEnvironment, normalizeRelPath } from './chunk-YLNQQZ5F.js';
9
9
  import path from 'node:path';
@@ -474,6 +474,7 @@ function deriveGateFindings(inv, disclosures = []) {
474
474
  finding({
475
475
  id: `${LENS_ID2}/hooks-not-wired`,
476
476
  tier: "risk",
477
+ kind: "truth",
477
478
  claim: "Tracked .githooks/ exist but git core.hooksPath is unset \u2014 the hooks never run",
478
479
  evidence: [".githooks/"],
479
480
  why: "Every check in those hooks is silently skipped on this clone.",
@@ -1329,9 +1330,7 @@ async function runAudit(root, opts = {}) {
1329
1330
  };
1330
1331
  }
1331
1332
  const profile = baseline?.profile ?? deriveProfile(facts);
1332
- const selected = LENSES.filter(
1333
- (lens) => (!opts.kind || lens.kind === opts.kind) && (!opts.lensIds || opts.lensIds.includes(lens.id))
1334
- );
1333
+ const selected = LENSES.filter((lens) => !opts.lensIds || opts.lensIds.includes(lens.id));
1335
1334
  const reports = [];
1336
1335
  for (const lens of selected) {
1337
1336
  try {
@@ -1349,11 +1348,12 @@ async function runAudit(root, opts = {}) {
1349
1348
  });
1350
1349
  }
1351
1350
  }
1352
- const all = reports.flatMap((r) => r.findings);
1351
+ const all = reports.flatMap((r) => r.findings.map((f) => ({ ...f, kind: f.kind ?? r.kind })));
1352
+ const visible = opts.kind ? all.filter((f) => f.kind === opts.kind) : all;
1353
1353
  const previous = await readLedger(ledgerRoot);
1354
1354
  const outOfScope = reports.flatMap((r) => r.outOfScope ?? []);
1355
1355
  const { ledger, diff } = reconcileLedger(previous, all, void 0, outOfScope);
1356
- const fullRun = selected.length === LENSES.length;
1356
+ const fullRun = !opts.kind && !opts.lensIds;
1357
1357
  const ledgerWritable = !opts.readOnlyRoot || ledgerRoot !== root;
1358
1358
  if ((opts.persistLedger ?? true) && fullRun && ledgerWritable) {
1359
1359
  await writeLedger(ledgerRoot, ledger);
@@ -1364,7 +1364,7 @@ async function runAudit(root, opts = {}) {
1364
1364
  baseline,
1365
1365
  config,
1366
1366
  reports,
1367
- findings: rankFindings(visibleFindings(all, previous)),
1367
+ findings: rankFindings(visibleFindings(visible, previous)),
1368
1368
  ledger,
1369
1369
  diff
1370
1370
  };
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { VERSION } from './chunk-NFENQ3IK.js';
2
+ import { VERSION } from './chunk-B3SUTZVY.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-GYZ5YZ7U.js');
34
+ const { run } = await import('./audit-IGIUGKSQ.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-OVLO4OBZ.js');
47
+ const { run } = await import('./init-WYVDRQ2J.js');
48
48
  await run({ cwd: resolveCwd(cmd), yes: opts.yes });
49
49
  })
50
50
  );
@@ -52,19 +52,19 @@ 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-JEOEC2MV.js');
55
+ const { run } = await import('./approve-P255DGOI.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-RAJUFTQR.js');
61
+ const { run } = await import('./scan-PZFF6LDA.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-C2RTL2QV.js');
67
+ const { run } = await import('./doctor-4LBUZHVD.js');
68
68
  await run({ cwd: resolveCwd(cmd), json: opts.json });
69
69
  })
70
70
  );
@@ -76,7 +76,7 @@ program.command("context").description("Measure the always-loaded context footpr
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-ZK7TXAC4.js');
79
+ const { run } = await import('./brief-SFZ2DR65.js');
80
80
  await run({ cwd: resolveCwd(cmd), human: opts.human });
81
81
  })
82
82
  );
@@ -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-YZGR4UC2.js');
114
+ const { sweep } = await import('./fleet-Q6J5XIHH.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-YZGR4UC2.js');
129
+ const { check } = await import('./fleet-Q6J5XIHH.js');
130
130
  const opts = cmd.optsWithGlobals();
131
131
  await check({ cwd: resolveCwd(cmd), manifest: opts.manifest, json: opts.json });
132
132
  })
@@ -136,7 +136,7 @@ fleet.command("add").argument("<dir>", "directory of the project to register").d
136
136
  "public-repo | public-bound | private \u2014 mandatory for personal entries"
137
137
  ).option("-y, --yes", "skip prompts; every mandatory value must be passed as a flag").option("--manifest <file>", "the fleet manifest \u2014 required unless the cwd holds registry.json").action(
138
138
  (dir, opts, cmd) => action(async () => {
139
- const { add } = await import('./fleet-YZGR4UC2.js');
139
+ const { add } = await import('./fleet-Q6J5XIHH.js');
140
140
  const shared = cmd.optsWithGlobals();
141
141
  await add({
142
142
  cwd: resolveCwd(cmd),
@@ -154,7 +154,7 @@ fleet.command("dismiss").argument("<name>", "the registered project name").argum
154
154
  "Dismiss a project's finding from any cwd \u2014 corp ledgers persist beside the manifest"
155
155
  ).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(
156
156
  (name, id, _opts, cmd) => action(async () => {
157
- const { dismiss } = await import('./fleet-YZGR4UC2.js');
157
+ const { dismiss } = await import('./fleet-Q6J5XIHH.js');
158
158
  const opts = cmd.optsWithGlobals();
159
159
  await dismiss({
160
160
  cwd: resolveCwd(cmd),
@@ -169,7 +169,7 @@ fleet.command("accept").argument("<name>", "the registered project name").argume
169
169
  "Accept a project's finding as a known trade-off \u2014 corp ledgers persist beside the manifest"
170
170
  ).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(
171
171
  (name, id, _opts, cmd) => action(async () => {
172
- const { accept } = await import('./fleet-YZGR4UC2.js');
172
+ const { accept } = await import('./fleet-Q6J5XIHH.js');
173
173
  const opts = cmd.optsWithGlobals();
174
174
  await accept({ cwd: resolveCwd(cmd), manifest: opts.manifest, name, id, reason: opts.reason });
175
175
  })
@@ -191,7 +191,7 @@ program.command("screen").description(
191
191
  );
192
192
  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(
193
193
  (opts, cmd) => action(async () => {
194
- const { run } = await import('./gates-ILE4PXDA.js');
194
+ const { run } = await import('./gates-T5J3H7WL.js');
195
195
  await run({ cwd: resolveCwd(cmd), ci: opts.ci, yes: opts.yes });
196
196
  })
197
197
  );
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
- import { run } from './chunk-XTAX4YNT.js';
3
- import './chunk-V3DPLOOM.js';
2
+ import { run } from './chunk-WOBHQQ6H.js';
3
+ import './chunk-WUGBAW6Z.js';
4
4
  import './chunk-LT67FU2Y.js';
5
5
  import './chunk-WWBF4Y7K.js';
6
6
  import './chunk-NMZ3RHWW.js';
7
7
  import './chunk-TADKOW6P.js';
8
+ import './chunk-7MOBQ4N5.js';
9
+ import './chunk-B3SUTZVY.js';
8
10
  import './chunk-R74SJ7HS.js';
9
- import './chunk-MKYPLNLH.js';
10
- import './chunk-NFENQ3IK.js';
11
11
  import './chunk-CRJHIXJH.js';
12
12
  import './chunk-YLNQQZ5F.js';
13
13
 
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import { dismiss, accept } from './chunk-MMPV67FW.js';
3
- import { parseFailOnTier, meetsFailOn, runAudit } from './chunk-V3DPLOOM.js';
3
+ import { parseFailOnTier, meetsFailOn, runAudit } from './chunk-WUGBAW6Z.js';
4
4
  import './chunk-LT67FU2Y.js';
5
5
  import { readLedger } from './chunk-WWBF4Y7K.js';
6
6
  import { DEFAULT_CONFIG } from './chunk-NMZ3RHWW.js';
7
7
  import { print, theme, section, renderFleetRows, renderFleetNotes, renderFindings, TIER_BADGE, glyph } from './chunk-TADKOW6P.js';
8
+ import { scanProject } from './chunk-7MOBQ4N5.js';
9
+ import './chunk-B3SUTZVY.js';
8
10
  import { ETYMD_DIR } from './chunk-R74SJ7HS.js';
9
- import { scanProject } from './chunk-MKYPLNLH.js';
10
- import './chunk-NFENQ3IK.js';
11
11
  import './chunk-CRJHIXJH.js';
12
12
  import { readText, pathExists, git, isDirectory } from './chunk-YLNQQZ5F.js';
13
13
  import { promises } from 'node:fs';
@@ -119,6 +119,7 @@ function loadRegistryEntries(projects, manifest, manifestFile) {
119
119
  // An unknown value is NOT coerced to a default: trust gates content screening, so a typo
120
120
  // must surface as "undeclared" (a finding) rather than quietly picking an answer.
121
121
  trust: isFleetTrust(rec.trust) ? rec.trust : void 0,
122
+ gates: asString(rec.gates),
122
123
  trustRaw: asString(rec.trust),
123
124
  staleAfterDays: typeof rec.staleAfterDays === "number" ? rec.staleAfterDays : void 0,
124
125
  stateBudget: typeof rec.stateBudget === "number" ? rec.stateBudget : void 0,
@@ -686,13 +687,17 @@ async function checkCorpEmails(manifest, findings, disclosures) {
686
687
  }
687
688
  }
688
689
  async function checkGateDrift(manifest, findings, disclosures) {
689
- const { planWorkflow } = await import('./generate-IYCQETQB.js');
690
- const { scanProject: scanProject2 } = await import('./scan-ZYZ7XDSJ.js');
690
+ const { planWorkflow } = await import('./generate-I75IEXXF.js');
691
+ const { scanProject: scanProject2 } = await import('./scan-IY3LCBJ6.js');
691
692
  const { readConfig } = await import('./config-MX7OYI7F.js');
692
693
  for (const entry of manifest.entries) {
693
694
  const root = entry.resolvedRoot;
694
695
  if (!root || !await isDirectory(root)) continue;
695
696
  if (entry.profile === "corp" || entry.private) continue;
697
+ if (entry.gates === "none") {
698
+ disclosures.push(`${entry.name}: gates declared \`none\` \u2014 deliberately absent, not missing`);
699
+ continue;
700
+ }
696
701
  const facts = await scanProject2(root);
697
702
  if (!facts.git.isRepo) continue;
698
703
  if (facts.hooks.source === "husky" || facts.hooks.source === "husky-legacy") {
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { readConfig, CONFIG_FILE, configPath } from './chunk-NMZ3RHWW.js';
3
2
  import { applyFiles } from './chunk-EF6BPBG6.js';
4
- import { isSafeGateCommand, planWorkflow, runPrefix } from './chunk-3NYDVB4Z.js';
3
+ import { derivedCommands, planWorkflow, runPrefix, isSafeGateCommand } from './chunk-VFUYQLW7.js';
4
+ import { readConfig, CONFIG_FILE, configPath } from './chunk-NMZ3RHWW.js';
5
5
  import { section, print, glyph, theme, renderPlan } from './chunk-TADKOW6P.js';
6
+ import { scanProject } from './chunk-7MOBQ4N5.js';
7
+ import './chunk-B3SUTZVY.js';
6
8
  import './chunk-R74SJ7HS.js';
7
- import { scanProject } from './chunk-MKYPLNLH.js';
8
- import './chunk-NFENQ3IK.js';
9
9
  import './chunk-CRJHIXJH.js';
10
10
  import { readText, git } from './chunk-YLNQQZ5F.js';
11
11
  import { promises } from 'node:fs';
@@ -55,16 +55,6 @@ var PUBLISH_GATE_VALUE = `./${PUBLISH_GATE_SCRIPT}`;
55
55
  function publishGateKey(route) {
56
56
  return route === "vscode" ? "vscode:prepublish" : "prepublishOnly";
57
57
  }
58
- function derivedCommands(facts, existingHook) {
59
- const c = facts.commands;
60
- const base = [c.formatCheck, c.typecheck, c.lint].filter(
61
- (k) => Boolean(k) && isSafeGateCommand(c.raw[k])
62
- );
63
- if (c.test && existingHook && new RegExp(`\\b${c.test}\\b`).test(existingHook)) {
64
- base.push(c.test);
65
- }
66
- return base;
67
- }
68
58
  function printGateSummary(gates, facts, source) {
69
59
  const run2 = runPrefix(facts.packageManager);
70
60
  const cmds = gates.commands.length ? gates.commands.map((c) => `${run2} ${c}`).join(", ") : "none detected";
@@ -269,4 +259,4 @@ async function run(opts) {
269
259
  }
270
260
  }
271
261
 
272
- export { derivedCommands, mergeGateSection, run };
262
+ export { mergeGateSection, run };
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ export { derivedCommands, planWorkflow } from './chunk-VFUYQLW7.js';
3
+ import './chunk-NMZ3RHWW.js';
4
+ import './chunk-R74SJ7HS.js';
5
+ import './chunk-CRJHIXJH.js';
6
+ import './chunk-YLNQQZ5F.js';
package/dist/index.d.ts CHANGED
@@ -279,6 +279,13 @@ interface Finding {
279
279
  id: string;
280
280
  lens: string;
281
281
  tier: FindingTier;
282
+ /**
283
+ * Whether this finding is an objective truth (the repo is lying) or an improvement opinion
284
+ * (something worth doing). Lives ON THE FINDING, not the lens: a lens can emit both kinds —
285
+ * gate-integrity is an improvement lens, but "hooks-not-wired" is an objective truth a `doctor`
286
+ * run must not skip. Defaulted from the lens kind when a finding does not set its own.
287
+ */
288
+ kind?: LensKind;
282
289
  /** One sentence: what is wrong / missing. */
283
290
  claim: string;
284
291
  /** File paths, job names, or metrics that ground the claim. Never empty. */
@@ -492,6 +499,15 @@ interface FleetEntry {
492
499
  /** A declared `trust` value outside the vocabulary — preserved verbatim so the check can name it. */
493
500
  trustRaw?: string;
494
501
  staleAfterDays?: number;
502
+ /**
503
+ * How this entry's git hooks are governed. `"none"` declares that generated gates are
504
+ * deliberately absent — a prose repo with nothing mechanically checkable, or one whose gate is
505
+ * hand-written on purpose. Wall findings cannot be quieted through the ledger (004: their only
506
+ * honest resolution is fixing them), which is right for leak and partition conditions but
507
+ * wrong for a state a fleet legitimately chooses; without this, a settled decision is
508
+ * re-reported on every sweep until the report is ignored.
509
+ */
510
+ gates?: string;
495
511
  /** Per-entry state char-budget override (ships unset — the schema slot exists). */
496
512
  stateBudget?: number;
497
513
  contract: FleetContract;
package/dist/index.js CHANGED
@@ -29,7 +29,7 @@ var init_package = __esm({
29
29
  "package.json"() {
30
30
  package_default = {
31
31
  name: "etymd",
32
- version: "0.3.0",
32
+ version: "0.3.2",
33
33
  description: "Keep your agent instructions true \u2014 verify AGENTS.md, CLAUDE.md, rules & skills against the actual repo, with drift caught over time and a regression ledger.",
34
34
  keywords: [
35
35
  "cli",
@@ -87,7 +87,7 @@ var init_package = __esm({
87
87
  "format:check": "prettier --check . && npm run guard:md",
88
88
  "guard:md": "! grep -rnF --include=*.md --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=.git '\\*\\*' .",
89
89
  prepare: "npm run build",
90
- prepublishOnly: "npm run build && artifact-check",
90
+ prepublishOnly: "npm run build && ./scripts/artifact-check.sh",
91
91
  changeset: "changeset",
92
92
  "version:packages": "changeset version",
93
93
  release: "npm run build && changeset publish"
@@ -1118,7 +1118,7 @@ exit 0
1118
1118
  function generatePrePushHook(facts, gates) {
1119
1119
  const run = runPrefix(facts.packageManager);
1120
1120
  const c = facts.commands;
1121
- const candidates = gates?.commands.length ? gates.commands : [c.formatCheck, c.typecheck, c.lint];
1121
+ const candidates = gates?.commands?.length ? gates.commands : [c.formatCheck, c.typecheck, c.lint];
1122
1122
  const allowed = new Set(gates?.allowWriting ?? []);
1123
1123
  const steps = candidates.filter(
1124
1124
  (key) => Boolean(key) && (allowed.has(key) || isSafeGateCommand(c.raw[key]))
@@ -1194,8 +1194,19 @@ var init_templates = __esm({
1194
1194
  // src/core/generate.ts
1195
1195
  var generate_exports = {};
1196
1196
  __export(generate_exports, {
1197
+ derivedCommands: () => derivedCommands,
1197
1198
  planWorkflow: () => planWorkflow
1198
1199
  });
1200
+ function derivedCommands(facts, existingHook) {
1201
+ const c = facts.commands;
1202
+ const base = [c.formatCheck, c.typecheck, c.lint].filter(
1203
+ (k) => Boolean(k) && isSafeGateCommand(c.raw[k])
1204
+ );
1205
+ if (c.test && existingHook && new RegExp(`\\b${c.test}\\b`).test(existingHook)) {
1206
+ base.push(c.test);
1207
+ }
1208
+ return base;
1209
+ }
1199
1210
  async function planWorkflow(root, facts, opts) {
1200
1211
  const out = [];
1201
1212
  const add = async (rel, contents, label, executable = false) => {
@@ -1215,6 +1226,7 @@ async function planWorkflow(root, facts, opts) {
1215
1226
  await add("AGENTS.md", generateAgentsMd(facts), "Minimal operating contract (scaffold)");
1216
1227
  }
1217
1228
  if (opts.gates) {
1229
+ const existingPrePush = await readText(path.join(root, ".githooks", "pre-push"));
1218
1230
  await add(".githooks/pre-commit", generatePreCommitHook(), "Process gate (pre-commit)", true);
1219
1231
  await add(
1220
1232
  ".githooks/commit-msg",
@@ -1222,9 +1234,15 @@ async function planWorkflow(root, facts, opts) {
1222
1234
  "Content screen (commit message)",
1223
1235
  true
1224
1236
  );
1237
+ const gateConfig = opts.gateConfig?.commands?.length ? opts.gateConfig : {
1238
+ commands: derivedCommands(facts, existingPrePush ?? void 0),
1239
+ failOn: opts.gateConfig?.failOn ?? DEFAULT_CONFIG.gates.failOn,
1240
+ publishGate: opts.gateConfig?.publishGate,
1241
+ allowWriting: opts.gateConfig?.allowWriting ?? []
1242
+ };
1225
1243
  await add(
1226
1244
  ".githooks/pre-push",
1227
- generatePrePushHook(facts, opts.gateConfig),
1245
+ generatePrePushHook(facts, gateConfig),
1228
1246
  "Correctness gate (pre-push)",
1229
1247
  true
1230
1248
  );
@@ -1242,6 +1260,7 @@ async function planWorkflow(root, facts, opts) {
1242
1260
  var init_generate = __esm({
1243
1261
  "src/core/generate.ts"() {
1244
1262
  init_templates();
1263
+ init_config();
1245
1264
  init_util();
1246
1265
  }
1247
1266
  });
@@ -1787,6 +1806,7 @@ function deriveGateFindings(inv, disclosures = []) {
1787
1806
  finding({
1788
1807
  id: `${LENS_ID2}/hooks-not-wired`,
1789
1808
  tier: "risk",
1809
+ kind: "truth",
1790
1810
  claim: "Tracked .githooks/ exist but git core.hooksPath is unset \u2014 the hooks never run",
1791
1811
  evidence: [".githooks/"],
1792
1812
  why: "Every check in those hooks is silently skipped on this clone.",
@@ -2750,9 +2770,7 @@ async function runAudit(root, opts = {}) {
2750
2770
  };
2751
2771
  }
2752
2772
  const profile = baseline?.profile ?? deriveProfile(facts);
2753
- const selected = LENSES.filter(
2754
- (lens) => (!opts.kind || lens.kind === opts.kind) && (!opts.lensIds || opts.lensIds.includes(lens.id))
2755
- );
2773
+ const selected = LENSES.filter((lens) => !opts.lensIds || opts.lensIds.includes(lens.id));
2756
2774
  const reports = [];
2757
2775
  for (const lens of selected) {
2758
2776
  try {
@@ -2770,11 +2788,12 @@ async function runAudit(root, opts = {}) {
2770
2788
  });
2771
2789
  }
2772
2790
  }
2773
- const all = reports.flatMap((r) => r.findings);
2791
+ const all = reports.flatMap((r) => r.findings.map((f) => ({ ...f, kind: f.kind ?? r.kind })));
2792
+ const visible = opts.kind ? all.filter((f) => f.kind === opts.kind) : all;
2774
2793
  const previous = await readLedger(ledgerRoot);
2775
2794
  const outOfScope = reports.flatMap((r) => r.outOfScope ?? []);
2776
2795
  const { ledger, diff } = reconcileLedger(previous, all, void 0, outOfScope);
2777
- const fullRun = selected.length === LENSES.length;
2796
+ const fullRun = !opts.kind && !opts.lensIds;
2778
2797
  const ledgerWritable = !opts.readOnlyRoot || ledgerRoot !== root;
2779
2798
  if ((opts.persistLedger ?? true) && fullRun && ledgerWritable) {
2780
2799
  await writeLedger(ledgerRoot, ledger);
@@ -2785,7 +2804,7 @@ async function runAudit(root, opts = {}) {
2785
2804
  baseline,
2786
2805
  config,
2787
2806
  reports,
2788
- findings: rankFindings(visibleFindings(all, previous)),
2807
+ findings: rankFindings(visibleFindings(visible, previous)),
2789
2808
  ledger,
2790
2809
  diff
2791
2810
  };
@@ -2895,6 +2914,7 @@ function loadRegistryEntries(projects, manifest, manifestFile) {
2895
2914
  // An unknown value is NOT coerced to a default: trust gates content screening, so a typo
2896
2915
  // must surface as "undeclared" (a finding) rather than quietly picking an answer.
2897
2916
  trust: isFleetTrust(rec.trust) ? rec.trust : void 0,
2917
+ gates: asString(rec.gates),
2898
2918
  trustRaw: asString(rec.trust),
2899
2919
  staleAfterDays: typeof rec.staleAfterDays === "number" ? rec.staleAfterDays : void 0,
2900
2920
  stateBudget: typeof rec.stateBudget === "number" ? rec.stateBudget : void 0,
@@ -3474,6 +3494,10 @@ async function checkGateDrift(manifest, findings, disclosures) {
3474
3494
  const root = entry.resolvedRoot;
3475
3495
  if (!root || !await isDirectory(root)) continue;
3476
3496
  if (entry.profile === "corp" || entry.private) continue;
3497
+ if (entry.gates === "none") {
3498
+ disclosures.push(`${entry.name}: gates declared \`none\` \u2014 deliberately absent, not missing`);
3499
+ continue;
3500
+ }
3477
3501
  const facts = await scanProject2(root);
3478
3502
  if (!facts.git.isRepo) continue;
3479
3503
  if (facts.hooks.source === "husky" || facts.hooks.source === "husky-legacy") {
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { applyFiles } from './chunk-EF6BPBG6.js';
3
- import { planWorkflow } from './chunk-3NYDVB4Z.js';
3
+ import { planWorkflow } from './chunk-VFUYQLW7.js';
4
+ import './chunk-NMZ3RHWW.js';
4
5
  import { theme, renderFacts, print, renderPlan, section, glyph } from './chunk-TADKOW6P.js';
6
+ import { scanProject } from './chunk-7MOBQ4N5.js';
7
+ import { VERSION } from './chunk-B3SUTZVY.js';
5
8
  import { writeCachedFacts, deriveProfile, writeBaseline, CACHE_DIR } from './chunk-R74SJ7HS.js';
6
- import { scanProject } from './chunk-MKYPLNLH.js';
7
- import { VERSION } from './chunk-NFENQ3IK.js';
8
9
  import { PACK_VERSION } from './chunk-CRJHIXJH.js';
9
10
  import { git, readText } from './chunk-YLNQQZ5F.js';
10
11
  import { promises } from 'node:fs';
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ export { scanProject } from './chunk-7MOBQ4N5.js';
3
+ import './chunk-B3SUTZVY.js';
4
+ import './chunk-CRJHIXJH.js';
5
+ import './chunk-YLNQQZ5F.js';
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { print, renderFacts, theme } from './chunk-TADKOW6P.js';
3
+ import { scanProject } from './chunk-7MOBQ4N5.js';
4
+ import './chunk-B3SUTZVY.js';
3
5
  import { writeCachedFacts } from './chunk-R74SJ7HS.js';
4
- import { scanProject } from './chunk-MKYPLNLH.js';
5
- import './chunk-NFENQ3IK.js';
6
6
  import './chunk-CRJHIXJH.js';
7
7
  import './chunk-YLNQQZ5F.js';
8
8
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "etymd",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
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",
@@ -58,7 +58,7 @@
58
58
  "format:check": "prettier --check . && npm run guard:md",
59
59
  "guard:md": "! grep -rnF --include=*.md --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=.git '\\*\\*' .",
60
60
  "prepare": "npm run build",
61
- "prepublishOnly": "npm run build && artifact-check",
61
+ "prepublishOnly": "npm run build && ./scripts/artifact-check.sh",
62
62
  "changeset": "changeset",
63
63
  "version:packages": "changeset version",
64
64
  "release": "npm run build && changeset publish"
@@ -1,4 +0,0 @@
1
- #!/usr/bin/env node
2
- export { planWorkflow } from './chunk-3NYDVB4Z.js';
3
- import './chunk-CRJHIXJH.js';
4
- import './chunk-YLNQQZ5F.js';
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env node
2
- export { scanProject } from './chunk-MKYPLNLH.js';
3
- import './chunk-NFENQ3IK.js';
4
- import './chunk-CRJHIXJH.js';
5
- import './chunk-YLNQQZ5F.js';