etymd 0.16.0 → 0.17.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # etymd
2
2
 
3
+ ## 0.17.0
4
+
5
+ ### Minor Changes
6
+
7
+ - The content screen's allow file accepts a `generated <path-regex>` record: a path declared
8
+ as regenerated data is still read and still screened, but vocabulary-class patterns drop
9
+ for it — secret-class patterns and the machine-path check stay — and the exempted paths
10
+ are named in the output, so a clean run never reads as fully screened. Provenance
11
+ (reason, date, author) is required.
12
+
13
+ ### Patch Changes
14
+
15
+ - 2bceef1: The fleet's gate-drift check now derives the pre-push audit tier the same way `etymd gates`
16
+ does. In a repo where no risk-tier rule can fire (no package manifest, no state doc) the
17
+ generator lowers the audit line to `--fail-on gap`; the drift comparison planned with the raw
18
+ config tier (`risk`), so its expectation permanently differed from the hook the generator
19
+ itself writes — a `gate-stale` finding on every repo of that shape, unclearable by the action
20
+ it names. The derivation now lives in `planWorkflow` itself, where the hook is built, so every
21
+ caller (the drift check, `etymd init`, the programmatic surface) generates identical bytes; a
22
+ tier the repo pinned in config is still never lowered.
23
+ - 7221fea: The content screen's machine-path rule now fires only at a path boundary (line start,
24
+ whitespace, a quote, `=`, `(`, `[` or `:`). A home path inside a longer token or a URL —
25
+ a test fixture's temp root (`$ROOT/home/‹x›/`), a docs URL with a home segment — is not a
26
+ machine path, and no longer prints the same advisory line on every push. A bare
27
+ `/Users/<name>/…` or `/home/<name>/…` still hits.
28
+
3
29
  ## 0.16.0
4
30
 
5
31
  ### Minor Changes
package/README.md CHANGED
@@ -375,6 +375,24 @@ missing a field is reported and does not apply: an exemption is a hole in the ga
375
375
  nobody signed cannot be audited later. The file is read from the repo being screened, never a
376
376
  shared location, and it screens itself out (it necessarily contains every string it exempts).
377
377
 
378
+ The file carries a second record kind, for paths rather than lines:
379
+
380
+ ```
381
+ generated ^src/data/corpus\.json$
382
+ reason public-domain corpus, regenerated by scripts/corpus each January
383
+ date 2026-09-04
384
+ author someone
385
+ ```
386
+
387
+ A `generated` path is data a pipeline rebuilds — a public-domain corpus, generated fixtures —
388
+ and period English trips credential vocabulary (`passkey`, `secret`) on a schedule, training
389
+ exactly the `--no-verify` habit the screen exists to prevent. The file is still read and still
390
+ screened: only vocabulary-class patterns drop for it, while secret-class patterns and the
391
+ machine-path check stay — a generated file is precisely where a real credential would be least
392
+ visible. Provenance is unconditional (a path is never the repo naming itself), and the paths
393
+ that took the exemption are named in the output, so a clean run never reads as fully screened
394
+ where it was screened narrower.
395
+
378
396
  ### The commit subject, if you ask for it
379
397
 
380
398
  Off unless you turn it on:
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { section, theme, print, renderBaselineDrift, glyph } from './chunk-HI7NWPRA.js';
3
- import { scanProject } from './chunk-EJM47PIF.js';
4
- import { VERSION } from './chunk-UYCGP5BS.js';
3
+ import { scanProject } from './chunk-446EZNAQ.js';
4
+ import { VERSION } from './chunk-BUMOGHQA.js';
5
5
  import { readBaseline, summarizeBaselineDrift, isDriftEmpty, writeBaseline, deriveProfile } from './chunk-P6ATKV2R.js';
6
6
  import { PACK_VERSION } from './chunk-YQZDYDAK.js';
7
7
  import './chunk-4VPBP6K6.js';
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
- export { run } from './chunk-FKDT4RBP.js';
3
- import './chunk-UV3YIBDW.js';
4
- import './chunk-Y4E6ERCM.js';
2
+ export { run } from './chunk-ANFNXFRI.js';
3
+ import './chunk-WKP7M2B3.js';
4
+ import './chunk-FNA4R5KT.js';
5
5
  import './chunk-3E2IPCRY.js';
6
6
  import './chunk-HI7NWPRA.js';
7
- import './chunk-EJM47PIF.js';
8
- import './chunk-UYCGP5BS.js';
7
+ import './chunk-446EZNAQ.js';
8
+ import './chunk-BUMOGHQA.js';
9
9
  import './chunk-P6ATKV2R.js';
10
10
  import './chunk-YQZDYDAK.js';
11
11
  import './chunk-4VPBP6K6.js';
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { print, theme } from './chunk-HI7NWPRA.js';
3
- import { scanProject } from './chunk-EJM47PIF.js';
4
- import './chunk-UYCGP5BS.js';
3
+ import { scanProject } from './chunk-446EZNAQ.js';
4
+ import './chunk-BUMOGHQA.js';
5
5
  import { ETYMD_DIR } from './chunk-P6ATKV2R.js';
6
6
  import './chunk-YQZDYDAK.js';
7
7
  import './chunk-4VPBP6K6.js';
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { VERSION } from './chunk-UYCGP5BS.js';
2
+ import { VERSION } from './chunk-BUMOGHQA.js';
3
3
  import { PACK_VERSION } from './chunk-YQZDYDAK.js';
4
4
  import { normalizeRelPath, isDirectory, readJson, git, matchesAnyGlob, pathExists, readText } from './chunk-4VPBP6K6.js';
5
5
  import path from 'path';
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { runAudit } from './chunk-UV3YIBDW.js';
3
- import { parseFailOnTier, meetsFailOn } from './chunk-Y4E6ERCM.js';
2
+ import { runAudit } from './chunk-WKP7M2B3.js';
3
+ import { parseFailOnTier, meetsFailOn } from './chunk-FNA4R5KT.js';
4
4
  import { print, section, theme, renderLensCoverage, renderFindings, renderLedgerDiff } from './chunk-HI7NWPRA.js';
5
5
 
6
6
  // src/commands/audit.ts
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // package.json
3
3
  var package_default = {
4
- version: "0.16.0"};
4
+ version: "0.17.0"};
5
5
 
6
6
  // src/version.ts
7
7
  var VERSION = package_default.version;
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { expandFileGlobs } from './chunk-EJM47PIF.js';
2
+ import { expandFileGlobs } from './chunk-446EZNAQ.js';
3
3
  import { DEFAULT_CONFIG, CONFIG_FILE } from './chunk-P6ATKV2R.js';
4
4
  import { readText, isDirectory, matchesAnyGlob, normalizeRelPath, readJson, pathExists, git } from './chunk-4VPBP6K6.js';
5
5
  import path3 from 'path';
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { runAudit } from './chunk-UV3YIBDW.js';
2
+ import { runAudit } from './chunk-WKP7M2B3.js';
3
3
  import { readLedger } from './chunk-3E2IPCRY.js';
4
4
  import { DEFAULT_CONFIG, ETYMD_DIR } from './chunk-P6ATKV2R.js';
5
5
  import { readText, isDirectory, git, pathExists } from './chunk-4VPBP6K6.js';
@@ -1005,8 +1005,8 @@ async function checkGuardedEmails(manifest, findings, disclosures) {
1005
1005
  }
1006
1006
  }
1007
1007
  async function checkGateDrift(manifest, findings, disclosures) {
1008
- const { planWorkflow } = await import('./generate-YEARCSHW.js');
1009
- const { scanProject } = await import('./scan-64Z6SNYF.js');
1008
+ const { planWorkflow } = await import('./generate-FSJQFQPV.js');
1009
+ const { scanProject } = await import('./scan-5SC47FSM.js');
1010
1010
  const { readConfig } = await import('./config-724Y3IOB.js');
1011
1011
  for (const entry of manifest.entries) {
1012
1012
  const root = entry.resolvedRoot;
@@ -1024,8 +1024,13 @@ async function checkGateDrift(manifest, findings, disclosures) {
1024
1024
  );
1025
1025
  continue;
1026
1026
  }
1027
- const { config } = await readConfig(root);
1028
- const planned = (await planWorkflow(root, facts, { agents: false, gates: true, gateConfig: config.gates })).filter((f) => f.executable);
1027
+ const { config, explicit } = await readConfig(root);
1028
+ const planned = (await planWorkflow(root, facts, {
1029
+ agents: false,
1030
+ gates: true,
1031
+ gateConfig: config.gates,
1032
+ gateFailOnPinned: explicit.gatesFailOn
1033
+ })).filter((f) => f.executable);
1029
1034
  const missing = planned.filter((f) => !f.exists).map((f) => f.path);
1030
1035
  const differing = planned.filter((f) => f.exists && f.differs);
1031
1036
  const stale = differing.filter((f) => f.drift !== "edited").map((f) => f.path);
@@ -431,9 +431,13 @@ async function planWorkflow(root, facts, opts) {
431
431
  commitFormat: opts.gateConfig?.commitFormat,
432
432
  allowWriting: opts.gateConfig?.allowWriting ?? []
433
433
  };
434
+ const tier = deriveFailOn(facts, {
435
+ failOn: gateConfig.failOn,
436
+ explicit: opts.gateFailOnPinned ?? false
437
+ });
434
438
  await add(
435
439
  ".githooks/pre-push",
436
- generatePrePushHook(facts, gateConfig, selfBuild),
440
+ generatePrePushHook(facts, { ...gateConfig, failOn: tier.failOn }, selfBuild),
437
441
  "Correctness gate (pre-push)",
438
442
  true
439
443
  );
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { stateFreshnessLens, rankFindings, listInstructionFiles, buildTruthEnv, emptyCounters, packageManagerUsage, checkDocRefs, listStateDocuments, loadDecisionLedger, checkDecisionRefs, checkTextClaims } from './chunk-Y4E6ERCM.js';
2
+ import { stateFreshnessLens, rankFindings, listInstructionFiles, buildTruthEnv, emptyCounters, packageManagerUsage, checkDocRefs, listStateDocuments, loadDecisionLedger, checkDecisionRefs, checkTextClaims } from './chunk-FNA4R5KT.js';
3
3
  import { readLedger, reconcileLedger, writeLedger, visibleFindings } from './chunk-3E2IPCRY.js';
4
4
  import { measureContext, contextFileLabel } from './chunk-HI7NWPRA.js';
5
- import { scanProject } from './chunk-EJM47PIF.js';
5
+ import { scanProject } from './chunk-446EZNAQ.js';
6
6
  import { DEFAULT_CONFIG, ETYMD_DIR, writeCachedFacts, readBaseline, readConfig, deriveProfile, baselineCarriesMachinePath, BASELINE_FILE, CONFIG_FILE } from './chunk-P6ATKV2R.js';
7
7
  import { PACK_VERSION } from './chunk-YQZDYDAK.js';
8
8
  import { pathExists, readText, readJson, isCiEnvironment, isExecutable } from './chunk-4VPBP6K6.js';
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { VERSION } from './chunk-UYCGP5BS.js';
2
+ import { VERSION } from './chunk-BUMOGHQA.js';
3
3
  import path from '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-HBNXNGD6.js');
34
+ const { run } = await import('./audit-PHAK5HEW.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 asked").option("-y, --yes", "accept defaults without prompting (never overwrites)").option("--with-agents", "also scaffold a minimal AGENTS.md where none exists (off by default)").action(
46
46
  (opts, cmd) => action(async () => {
47
- const { run } = await import('./init-Z5O2WVRJ.js');
47
+ const { run } = await import('./init-6RULX5JZ.js');
48
48
  await run({ cwd: resolveCwd(cmd), yes: opts.yes, withAgents: opts.withAgents });
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-K5FFTPA4.js');
55
+ const { run } = await import('./approve-VN4EDBR5.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-H3NF5IV5.js');
61
+ const { run } = await import('./scan-LLWW4YOU.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-X6XAGKV7.js');
67
+ const { run } = await import('./doctor-R4QDCAN5.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-DOQ2DQEG.js');
79
+ const { run } = await import('./brief-7MWP35UI.js');
80
80
  await run({ cwd: resolveCwd(cmd), human: opts.human });
81
81
  })
82
82
  );
@@ -90,7 +90,7 @@ program.command("premise").argument("[task]", "the task you are about to hand an
90
90
  "exit non-zero when findings at/above this tier exist (risk|gap|polish)"
91
91
  ).action(
92
92
  (task, opts, cmd) => action(async () => {
93
- const { run } = await import('./premise-4QRU7LRI.js');
93
+ const { run } = await import('./premise-GFWHSR3K.js');
94
94
  await run({
95
95
  cwd: resolveCwd(cmd),
96
96
  task,
@@ -132,7 +132,7 @@ var fleet = program.command("fleet").description(
132
132
  "exit non-zero when findings at/above this tier exist (risk|gap|polish)"
133
133
  ).action(
134
134
  (opts, cmd) => action(async () => {
135
- const { sweep } = await import('./fleet-UEFLKIRI.js');
135
+ const { sweep } = await import('./fleet-WPYEVHVL.js');
136
136
  await sweep({
137
137
  cwd: resolveCwd(cmd),
138
138
  manifest: opts.manifest,
@@ -147,7 +147,7 @@ var fleet = program.command("fleet").description(
147
147
  );
148
148
  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(
149
149
  (_opts, cmd) => action(async () => {
150
- const { check } = await import('./fleet-UEFLKIRI.js');
150
+ const { check } = await import('./fleet-WPYEVHVL.js');
151
151
  const opts = cmd.optsWithGlobals();
152
152
  await check({ cwd: resolveCwd(cmd), manifest: opts.manifest, json: opts.json });
153
153
  })
@@ -156,7 +156,7 @@ fleet.command("board").description(
156
156
  "Render the fleet board: every project's milestones (contract key `milestones`) plus a ranked initiatives table"
157
157
  ).option("--manifest <file>", "the fleet manifest \u2014 required unless the cwd holds registry.json").option("--initiatives <file>", "the hand-edited initiatives table (rank | id | initiative | \u2026)").option("--out <file>", "write the Markdown board here instead of printing it").option("--json", "print the board as JSON (EXPERIMENTAL)").action(
158
158
  (opts, cmd) => action(async () => {
159
- const { board } = await import('./fleet-UEFLKIRI.js');
159
+ const { board } = await import('./fleet-WPYEVHVL.js');
160
160
  const shared = cmd.optsWithGlobals();
161
161
  await board({
162
162
  cwd: resolveCwd(cmd),
@@ -172,7 +172,7 @@ fleet.command("add").argument("<dir>", "directory of the project to register").d
172
172
  "public-repo | public-bound | private \u2014 mandatory for personal entries"
173
173
  ).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(
174
174
  (dir, opts, cmd) => action(async () => {
175
- const { add } = await import('./fleet-UEFLKIRI.js');
175
+ const { add } = await import('./fleet-WPYEVHVL.js');
176
176
  const shared = cmd.optsWithGlobals();
177
177
  await add({
178
178
  cwd: resolveCwd(cmd),
@@ -195,7 +195,7 @@ fleet.command("dismiss").argument("<name>", "the registered project name").argum
195
195
  "Dismiss a project's finding from any cwd \u2014 guarded ledgers persist beside the manifest"
196
196
  ).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(
197
197
  (name, id, _opts, cmd) => action(async () => {
198
- const { dismiss } = await import('./fleet-UEFLKIRI.js');
198
+ const { dismiss } = await import('./fleet-WPYEVHVL.js');
199
199
  const opts = cmd.optsWithGlobals();
200
200
  await dismiss({
201
201
  cwd: resolveCwd(cmd),
@@ -210,7 +210,7 @@ fleet.command("accept").argument("<name>", "the registered project name").argume
210
210
  "Accept a project's finding as a known trade-off \u2014 guarded ledgers persist beside the manifest"
211
211
  ).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(
212
212
  (name, id, _opts, cmd) => action(async () => {
213
- const { accept } = await import('./fleet-UEFLKIRI.js');
213
+ const { accept } = await import('./fleet-WPYEVHVL.js');
214
214
  const opts = cmd.optsWithGlobals();
215
215
  await accept({ cwd: resolveCwd(cmd), manifest: opts.manifest, name, id, reason: opts.reason });
216
216
  })
@@ -225,7 +225,7 @@ program.command("propose").description(
225
225
  "the fleet manifest \u2014 runs a read-only sweep now (no delta baseline move)"
226
226
  ).option("--from <file>", "read a stored `etymd fleet --json` output instead of sweeping").option("--json", "print the proposal/1 records as JSON (EXPERIMENTAL)").action(
227
227
  (opts, cmd) => action(async () => {
228
- const { run } = await import('./propose-IKE4CUIM.js');
228
+ const { run } = await import('./propose-IYNOIWAO.js');
229
229
  await run({
230
230
  cwd: resolveCwd(cmd),
231
231
  manifest: opts.manifest,
@@ -242,7 +242,7 @@ program.command("screen").description(
242
242
  "fork's upstream remote: files byte-identical to its refs skip vocabulary-class patterns (secret patterns stay absolute)"
243
243
  ).action(
244
244
  (opts, cmd) => action(async () => {
245
- const { run } = await import('./screen-E4FC7W5N.js');
245
+ const { run } = await import('./screen-S736DXQ5.js');
246
246
  const scope = opts.message ? "message" : opts.dir ? "dir" : opts.tree ? "tree" : "staged";
247
247
  await run({
248
248
  cwd: resolveCwd(cmd),
@@ -256,7 +256,7 @@ program.command("screen").description(
256
256
  );
257
257
  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(
258
258
  (opts, cmd) => action(async () => {
259
- const { run } = await import('./gates-NVWWKMT2.js');
259
+ const { run } = await import('./gates-3HRGYCYY.js');
260
260
  await run({ cwd: resolveCwd(cmd), ci: opts.ci, yes: opts.yes });
261
261
  })
262
262
  );
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { run } from './chunk-FKDT4RBP.js';
3
- import './chunk-UV3YIBDW.js';
4
- import './chunk-Y4E6ERCM.js';
2
+ import { run } from './chunk-ANFNXFRI.js';
3
+ import './chunk-WKP7M2B3.js';
4
+ import './chunk-FNA4R5KT.js';
5
5
  import './chunk-3E2IPCRY.js';
6
6
  import './chunk-HI7NWPRA.js';
7
- import './chunk-EJM47PIF.js';
8
- import './chunk-UYCGP5BS.js';
7
+ import './chunk-446EZNAQ.js';
8
+ import './chunk-BUMOGHQA.js';
9
9
  import './chunk-P6ATKV2R.js';
10
10
  import './chunk-YQZDYDAK.js';
11
11
  import './chunk-4VPBP6K6.js';
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { dismiss, accept } from './chunk-HOR4M6EC.js';
3
- import { sweepFleet, FLEET_JSON_SCHEMA, checkManifest, FLEET_TRUST_VALUES, isFleetTrust, MILESTONES_FILE, parseMilestones, parseInitiatives, BOARD_JSON_SCHEMA, renderBoard, loadFleetManifest, fleetLedgerTarget, ensureFindingRecorded } from './chunk-ANXGT7V3.js';
4
- import './chunk-UV3YIBDW.js';
5
- import { parseFailOnTier, meetsFailOn } from './chunk-Y4E6ERCM.js';
3
+ import { sweepFleet, FLEET_JSON_SCHEMA, checkManifest, FLEET_TRUST_VALUES, isFleetTrust, MILESTONES_FILE, parseMilestones, parseInitiatives, BOARD_JSON_SCHEMA, renderBoard, loadFleetManifest, fleetLedgerTarget, ensureFindingRecorded } from './chunk-LVR2DPR7.js';
4
+ import './chunk-WKP7M2B3.js';
5
+ import { parseFailOnTier, meetsFailOn } from './chunk-FNA4R5KT.js';
6
6
  import './chunk-3E2IPCRY.js';
7
7
  import { print, theme, section, renderFleetRows, renderFleetNotes, renderFindings, TIER_BADGE, glyph } from './chunk-HI7NWPRA.js';
8
- import { scanProject } from './chunk-EJM47PIF.js';
9
- import './chunk-UYCGP5BS.js';
8
+ import { scanProject } from './chunk-446EZNAQ.js';
9
+ import './chunk-BUMOGHQA.js';
10
10
  import './chunk-P6ATKV2R.js';
11
11
  import './chunk-YQZDYDAK.js';
12
12
  import { readText, pathExists, git } from './chunk-4VPBP6K6.js';
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { applyFiles } from './chunk-GWGKEPRX.js';
3
- import { deriveFailOn, derivedCommands, planWorkflow, runPrefix, isSafeGateCommand } from './chunk-OKJ3RGJB.js';
3
+ import { deriveFailOn, derivedCommands, planWorkflow, runPrefix, isSafeGateCommand } from './chunk-PIJZUDDQ.js';
4
4
  import { section, print, glyph, theme, renderPlan } from './chunk-HI7NWPRA.js';
5
- import { scanProject } from './chunk-EJM47PIF.js';
6
- import './chunk-UYCGP5BS.js';
5
+ import { scanProject } from './chunk-446EZNAQ.js';
6
+ import './chunk-BUMOGHQA.js';
7
7
  import { readConfig, CONFIG_FILE, configPath } from './chunk-P6ATKV2R.js';
8
8
  import './chunk-YQZDYDAK.js';
9
9
  import { readText, git } from './chunk-4VPBP6K6.js';
@@ -202,7 +202,10 @@ async function run(opts) {
202
202
  const files = await planWorkflow(opts.cwd, facts, {
203
203
  agents: false,
204
204
  gates: true,
205
- gateConfig
205
+ gateConfig,
206
+ // A pinned tier is a decision planWorkflow must not lower; an unpinned one it derives —
207
+ // the same flag the fleet's drift comparison passes, so both generate identical bytes.
208
+ gateFailOnPinned: explicit.gatesFailOn
206
209
  });
207
210
  return files.filter((f) => f.executable);
208
211
  };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- export { deriveFailOn, derivedCommands, planWorkflow, riskReachability } from './chunk-OKJ3RGJB.js';
2
+ export { deriveFailOn, derivedCommands, planWorkflow, riskReachability } from './chunk-PIJZUDDQ.js';
3
3
  import './chunk-P6ATKV2R.js';
4
4
  import './chunk-YQZDYDAK.js';
5
5
  import './chunk-4VPBP6K6.js';
package/dist/index.d.ts CHANGED
@@ -303,6 +303,14 @@ interface PlanOptions {
303
303
  publishGate?: boolean;
304
304
  /** Recorded gate choices; absent means derive everything from the scan. */
305
305
  gateConfig?: GateConfig;
306
+ /**
307
+ * Whether `gateConfig.failOn` was PINNED by the repo's committed config, versus a default the
308
+ * config loader filled in. The tier derivation may only choose between defaults — a recorded
309
+ * tier is a decision and is never lowered (see `deriveFailOn`). Callers that read the config
310
+ * pass `explicit.gatesFailOn` from `readConfig`; callers planning from a bare scan leave it
311
+ * unset, which is the same as unpinned.
312
+ */
313
+ gateFailOnPinned?: boolean;
306
314
  }
307
315
  /** Build the file set an onboarding would write, flagging which exist and which differ. */
308
316
  declare function planWorkflow(root: string, facts: ProjectFacts, opts: PlanOptions): Promise<GeneratedFile[]>;
package/dist/index.js CHANGED
@@ -30,7 +30,7 @@ var init_package = __esm({
30
30
  "package.json"() {
31
31
  package_default = {
32
32
  name: "etymd",
33
- version: "0.16.0",
33
+ version: "0.17.0",
34
34
  description: "Keep your agent instructions true \u2014 verify AGENTS.md, CLAUDE.md, rules, skills, and the task you hand an agent against the actual repo, with drift caught over time and a regression ledger.",
35
35
  keywords: [
36
36
  "cli",
@@ -1449,9 +1449,13 @@ async function planWorkflow(root, facts, opts) {
1449
1449
  commitFormat: opts.gateConfig?.commitFormat,
1450
1450
  allowWriting: opts.gateConfig?.allowWriting ?? []
1451
1451
  };
1452
+ const tier = deriveFailOn(facts, {
1453
+ failOn: gateConfig.failOn,
1454
+ explicit: opts.gateFailOnPinned ?? false
1455
+ });
1452
1456
  await add(
1453
1457
  ".githooks/pre-push",
1454
- generatePrePushHook(facts, gateConfig, selfBuild),
1458
+ generatePrePushHook(facts, { ...gateConfig, failOn: tier.failOn }, selfBuild),
1455
1459
  "Correctness gate (pre-push)",
1456
1460
  true
1457
1461
  );
@@ -4307,8 +4311,13 @@ async function checkGateDrift(manifest, findings, disclosures) {
4307
4311
  );
4308
4312
  continue;
4309
4313
  }
4310
- const { config } = await readConfig2(root);
4311
- const planned = (await planWorkflow2(root, facts, { agents: false, gates: true, gateConfig: config.gates })).filter((f) => f.executable);
4314
+ const { config, explicit } = await readConfig2(root);
4315
+ const planned = (await planWorkflow2(root, facts, {
4316
+ agents: false,
4317
+ gates: true,
4318
+ gateConfig: config.gates,
4319
+ gateFailOnPinned: explicit.gatesFailOn
4320
+ })).filter((f) => f.executable);
4312
4321
  const missing = planned.filter((f) => !f.exists).map((f) => f.path);
4313
4322
  const differing = planned.filter((f) => f.exists && f.differs);
4314
4323
  const stale = differing.filter((f) => f.drift !== "edited").map((f) => f.path);
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { applyFiles } from './chunk-GWGKEPRX.js';
3
- import { planWorkflow } from './chunk-OKJ3RGJB.js';
3
+ import { planWorkflow } from './chunk-PIJZUDDQ.js';
4
4
  import { theme, renderFacts, print, renderPlan, section, glyph } from './chunk-HI7NWPRA.js';
5
- import { scanProject } from './chunk-EJM47PIF.js';
6
- import { VERSION } from './chunk-UYCGP5BS.js';
5
+ import { scanProject } from './chunk-446EZNAQ.js';
6
+ import { VERSION } from './chunk-BUMOGHQA.js';
7
7
  import { writeCachedFacts, deriveProfile, writeBaseline, CACHE_DIR } from './chunk-P6ATKV2R.js';
8
8
  import { PACK_VERSION } from './chunk-YQZDYDAK.js';
9
9
  import { git, readText } from './chunk-4VPBP6K6.js';
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { parseFailOnTier, meetsFailOn, buildTruthEnv, emptyCounters, checkTextClaims, checkDocRefs, loadDecisionLedger, checkDecisionRefs, rankFindings, PATH_TOKEN_RE, KNOWN_EXTENSIONS, NAMESPACE_IDENT, NAMESPACE_STOP } from './chunk-Y4E6ERCM.js';
2
+ import { parseFailOnTier, meetsFailOn, buildTruthEnv, emptyCounters, checkTextClaims, checkDocRefs, loadDecisionLedger, checkDecisionRefs, rankFindings, PATH_TOKEN_RE, KNOWN_EXTENSIONS, NAMESPACE_IDENT, NAMESPACE_STOP } from './chunk-FNA4R5KT.js';
3
3
  import { print, section, theme, renderFindings } from './chunk-HI7NWPRA.js';
4
- import { scanProject } from './chunk-EJM47PIF.js';
5
- import './chunk-UYCGP5BS.js';
4
+ import { scanProject } from './chunk-446EZNAQ.js';
5
+ import './chunk-BUMOGHQA.js';
6
6
  import { ETYMD_DIR } from './chunk-P6ATKV2R.js';
7
7
  import './chunk-YQZDYDAK.js';
8
8
  import { readText, pathExists } from './chunk-4VPBP6K6.js';
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { sweepFleet, loadFleetManifest, FLEET_JSON_SCHEMA } from './chunk-ANXGT7V3.js';
3
- import './chunk-UV3YIBDW.js';
4
- import './chunk-Y4E6ERCM.js';
2
+ import { sweepFleet, loadFleetManifest, FLEET_JSON_SCHEMA } from './chunk-LVR2DPR7.js';
3
+ import './chunk-WKP7M2B3.js';
4
+ import './chunk-FNA4R5KT.js';
5
5
  import './chunk-3E2IPCRY.js';
6
6
  import { print, section, theme, renderFleetNotes } from './chunk-HI7NWPRA.js';
7
- import './chunk-EJM47PIF.js';
8
- import './chunk-UYCGP5BS.js';
7
+ import './chunk-446EZNAQ.js';
8
+ import './chunk-BUMOGHQA.js';
9
9
  import './chunk-P6ATKV2R.js';
10
10
  import './chunk-YQZDYDAK.js';
11
11
  import { readText } from './chunk-4VPBP6K6.js';
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ export { scanProject } from './chunk-446EZNAQ.js';
3
+ import './chunk-BUMOGHQA.js';
4
+ import './chunk-YQZDYDAK.js';
5
+ import './chunk-4VPBP6K6.js';
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { print, renderFacts, theme } from './chunk-HI7NWPRA.js';
3
- import { scanProject } from './chunk-EJM47PIF.js';
4
- import './chunk-UYCGP5BS.js';
3
+ import { scanProject } from './chunk-446EZNAQ.js';
4
+ import './chunk-BUMOGHQA.js';
5
5
  import { writeCachedFacts } from './chunk-P6ATKV2R.js';
6
6
  import './chunk-YQZDYDAK.js';
7
7
  import './chunk-4VPBP6K6.js';
@@ -6,17 +6,28 @@ import { promises } from 'fs';
6
6
  import path from 'path';
7
7
 
8
8
  var CLASS_DIRECTIVE = /^#[ \t]*class:[ \t]*(secret|vocabulary)\b/i;
9
- var MACHINE_PATH_RE = /\/(?:Users|home)\/[A-Za-z0-9._-]+\//;
9
+ var MACHINE_PATH_RE = /(?:^|[\s"'=(\[:])\/(?:Users|home)\/[A-Za-z0-9._-]+\//;
10
10
  var ALLOW_MARKER = "allow-published-string";
11
11
  var ALLOW_FILE = ".etymd-screen-allow";
12
12
  var LEGACY_ALLOW_FILE = ".artifact-check-allow";
13
- var LABELED_LINE = /^(pattern|reason|date|author)[ \t]+(.*)$/;
13
+ var LABELED_LINE = /^(pattern|generated|reason|date|author)[ \t]+(.*)$/;
14
+ function compileLooseRegex(source) {
15
+ try {
16
+ return new RegExp(source, "i");
17
+ } catch {
18
+ return new RegExp(source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i");
19
+ }
20
+ }
14
21
  function compileAllow(raw) {
15
- const entries = [];
22
+ const lines = [];
23
+ const generated = [];
16
24
  let current = null;
25
+ let currentGen = null;
17
26
  const close = () => {
18
- if (current) entries.push(current);
27
+ if (current) lines.push(current);
28
+ if (currentGen) generated.push(currentGen);
19
29
  current = null;
30
+ currentGen = null;
20
31
  };
21
32
  for (const rawLine of raw.split("\n")) {
22
33
  const l = rawLine.trim();
@@ -26,29 +37,26 @@ function compileAllow(raw) {
26
37
  const [, label = "", value = ""] = labeled;
27
38
  if (label === "pattern") {
28
39
  close();
29
- current = makeAllowEntry(value);
40
+ current = { pattern: compileLooseRegex(value), isSelfName: false };
41
+ } else if (label === "generated") {
42
+ close();
43
+ currentGen = { path: compileLooseRegex(value) };
30
44
  } else if (current) {
31
45
  if (label === "reason") current.reason = value.trim();
32
46
  else if (label === "date") current.date = value.trim();
33
47
  else current.author = value.trim();
48
+ } else if (currentGen) {
49
+ if (label === "reason") currentGen.reason = value.trim();
50
+ else if (label === "date") currentGen.date = value.trim();
51
+ else currentGen.author = value.trim();
34
52
  }
35
53
  } else {
36
54
  close();
37
- current = makeAllowEntry(l);
55
+ current = { pattern: compileLooseRegex(l), isSelfName: false };
38
56
  }
39
57
  }
40
58
  close();
41
- return entries;
42
- }
43
- function makeAllowEntry(patternStr) {
44
- try {
45
- return { pattern: new RegExp(patternStr, "i"), isSelfName: false };
46
- } catch {
47
- return {
48
- pattern: new RegExp(patternStr.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i"),
49
- isSelfName: false
50
- };
51
- }
59
+ return { lines, generated };
52
60
  }
53
61
  function compileOnePattern(l) {
54
62
  try {
@@ -117,11 +125,11 @@ async function selfIdentities(cwd) {
117
125
  names.delete("");
118
126
  return [...names];
119
127
  }
120
- function screenText(text, file, patterns, allow = [], upstreamOwned = false) {
128
+ function screenText(text, file, patterns, allow = [], upstreamOwned = false, generatedData = false) {
121
129
  const classed = patterns.map(
122
130
  (p) => p instanceof RegExp ? { re: p, cls: "secret" } : p
123
131
  );
124
- const active = upstreamOwned ? classed.filter((p) => p.cls === "secret") : classed;
132
+ const active = upstreamOwned || generatedData ? classed.filter((p) => p.cls === "secret") : classed;
125
133
  const hits = [];
126
134
  const lines = text.split("\n");
127
135
  for (const [i, line] of lines.entries()) {
@@ -213,9 +221,9 @@ async function run(opts) {
213
221
  }
214
222
  const usingLegacy = !await pathExists(path.join(opts.cwd, ALLOW_FILE)) && await pathExists(path.join(opts.cwd, LEGACY_ALLOW_FILE));
215
223
  const allowRaw = usingLegacy ? await readText(path.join(opts.cwd, LEGACY_ALLOW_FILE)) : await readText(path.join(opts.cwd, ALLOW_FILE));
216
- const allowEntries = allowRaw ? compileAllow(allowRaw) : [];
224
+ const allowFile = allowRaw ? compileAllow(allowRaw) : { lines: [], generated: [] };
217
225
  const selves = await selfIdentities(opts.cwd);
218
- const validatedEntries = allowEntries.filter((entry) => {
226
+ const validatedEntries = allowFile.lines.filter((entry) => {
219
227
  if (entry.isSelfName) return true;
220
228
  if (isSelfName(entry.pattern, selves)) {
221
229
  entry.isSelfName = true;
@@ -233,6 +241,17 @@ async function run(opts) {
233
241
  return true;
234
242
  });
235
243
  const allow = validatedEntries;
244
+ const generatedPaths = allowFile.generated.filter((entry) => {
245
+ if (entry.reason && entry.date && entry.author) return true;
246
+ print(
247
+ ` ${glyph.partial} ${theme.warn(`Missing provenance for generated-path exemption`)} ${theme.dim(String(entry.path))}`
248
+ );
249
+ print(
250
+ ` ${theme.dim("Format: a `generated` line, then `reason`, `date`, `author` lines \u2014 all four required")}`
251
+ );
252
+ return false;
253
+ });
254
+ const isGenerated = (rel) => generatedPaths.some((e) => e.path.test(rel));
236
255
  const active = selves.length ? patterns.filter((p) => !isSelfName(p.re, selves)) : patterns;
237
256
  const hasVocab = active.some((p) => p.cls === "vocabulary");
238
257
  const upstreamRemote = hasVocab && (opts.scope === "staged" || opts.scope === "tree") ? opts.upstream ?? await git(opts.cwd, ["config", "--get", "etymd.upstream"]) ?? void 0 : void 0;
@@ -263,6 +282,7 @@ async function run(opts) {
263
282
  const hits = [];
264
283
  let scanned = 0;
265
284
  let skippedBinary = 0;
285
+ const generatedExempted = [];
266
286
  if (opts.scope === "message") {
267
287
  const file = opts.target;
268
288
  if (!file) throw new Error("--message needs the message file path (the commit-msg hook's $1)");
@@ -284,7 +304,10 @@ async function run(opts) {
284
304
  continue;
285
305
  }
286
306
  scanned++;
287
- hits.push(...screenText(raw, path.relative(dir, f), active, allow));
307
+ const rel = path.relative(dir, f);
308
+ const gen = isGenerated(rel);
309
+ if (gen) generatedExempted.push(rel);
310
+ hits.push(...screenText(raw, rel, active, allow, false, gen));
288
311
  }
289
312
  } else {
290
313
  const listing = opts.scope === "tree" ? await git(opts.cwd, ["ls-files"]) : await git(opts.cwd, ["diff", "--cached", "--name-only", "--diff-filter=ACM"]);
@@ -300,7 +323,9 @@ async function run(opts) {
300
323
  continue;
301
324
  }
302
325
  scanned++;
303
- hits.push(...screenText(raw, rel, active, allow, isUpstreamOwned(rel)));
326
+ const gen = isGenerated(rel);
327
+ if (gen) generatedExempted.push(rel);
328
+ hits.push(...screenText(raw, rel, active, allow, isUpstreamOwned(rel), gen));
304
329
  }
305
330
  }
306
331
  if (upstreamNote) {
@@ -308,9 +333,16 @@ async function run(opts) {
308
333
  }
309
334
  section(
310
335
  `Content screen ${theme.dim(
311
- `\xB7 ${opts.scope} \xB7 ${scanned} file(s)${skippedBinary ? ` \xB7 ${skippedBinary} binary skipped` : ""}`
336
+ `\xB7 ${opts.scope} \xB7 ${scanned} file(s)${skippedBinary ? ` \xB7 ${skippedBinary} binary skipped` : ""}${generatedExempted.length ? ` \xB7 ${generatedExempted.length} generated exempt` : ""}`
312
337
  )}`
313
338
  );
339
+ if (generatedExempted.length) {
340
+ print(
341
+ ` ${glyph.partial} ${theme.dim(
342
+ `generated data, vocabulary-class dropped (secret-class and machine paths still screened): ${generatedExempted.join(", ")}`
343
+ )}`
344
+ );
345
+ }
314
346
  if (!hits.length) {
315
347
  print(` ${glyph.ok} ${theme.dim("clean")}`);
316
348
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "etymd",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Keep your agent instructions true — verify AGENTS.md, CLAUDE.md, rules, skills, and the task you hand an agent against the actual repo, with drift caught over time and a regression ledger.",
5
5
  "keywords": [
6
6
  "cli",
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env node
2
- export { scanProject } from './chunk-EJM47PIF.js';
3
- import './chunk-UYCGP5BS.js';
4
- import './chunk-YQZDYDAK.js';
5
- import './chunk-4VPBP6K6.js';