etymd 0.11.0 → 0.12.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.12.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 060c50b: screen: forks can exempt upstream-owned files from vocabulary-class patterns
8
+
9
+ A fork that syncs from upstream stages hundreds of upstream files; a pattern that
10
+ matches ordinary wording (a "vocabulary" pattern, e.g. words a leaky _description_
11
+ would use) then fires on upstream's own already-public docs and blocks the sync
12
+ commit. This adds:
13
+
14
+ - **Pattern classes.** A `# class: vocabulary` directive in the pattern file marks
15
+ the patterns beneath it as vocabulary; everything else stays `secret`. Files with
16
+ no directive are unchanged — every pattern absolute.
17
+ - **Upstream ownership by blob identity.** With `--upstream <remote>` or
18
+ `git config etymd.upstream <remote>`, a staged/tree file whose content matches any
19
+ blob at the tips of `refs/remotes/<remote>/*` is upstream-owned, and its
20
+ vocabulary-class patterns are skipped. Secret-class patterns and the machine-path
21
+ check stay absolute everywhere. Matching is by blob sha across all upstream refs,
22
+ so a file copied verbatim to a different path (an adapter lifted from another
23
+ upstream branch) is still recognised, and a file the fork _modifies_ is screened
24
+ in full.
25
+ - **Fails closed and loud.** If the upstream ref cannot be read (a rebuild dropped
26
+ the remote), nothing is exempted and a one-line notice says so — the config signal
27
+ persists so the fork still knows it is a fork.
28
+
3
29
  ## 0.11.0
4
30
 
5
31
  ### Minor Changes
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { section, theme, print, renderBaselineDrift, glyph } from './chunk-5BVKFJWM.js';
3
- import { scanProject } from './chunk-5GQ52DK6.js';
4
- import { VERSION } from './chunk-J7XVOTZL.js';
3
+ import { scanProject } from './chunk-MTVEJ33D.js';
4
+ import { VERSION } from './chunk-YFNRDXUY.js';
5
5
  import { readBaseline, summarizeBaselineDrift, isDriftEmpty, writeBaseline, deriveProfile } from './chunk-JHZ2BN4U.js';
6
6
  import { PACK_VERSION } from './chunk-LSZGCKIQ.js';
7
7
  import './chunk-4VPBP6K6.js';
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- export { run } from './chunk-2KVQBL4E.js';
3
- import './chunk-JVLWM27I.js';
2
+ export { run } from './chunk-SWA4NQHB.js';
3
+ import './chunk-O3PHW4OP.js';
4
4
  import './chunk-F75Q43BC.js';
5
5
  import './chunk-3E2IPCRY.js';
6
6
  import './chunk-DWL2IKZH.js';
7
7
  import './chunk-5BVKFJWM.js';
8
- import './chunk-5GQ52DK6.js';
9
- import './chunk-J7XVOTZL.js';
8
+ import './chunk-MTVEJ33D.js';
9
+ import './chunk-YFNRDXUY.js';
10
10
  import './chunk-JHZ2BN4U.js';
11
11
  import './chunk-LSZGCKIQ.js';
12
12
  import './chunk-4VPBP6K6.js';
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { print, theme } from './chunk-5BVKFJWM.js';
3
- import { scanProject } from './chunk-5GQ52DK6.js';
4
- import './chunk-J7XVOTZL.js';
3
+ import { scanProject } from './chunk-MTVEJ33D.js';
4
+ import './chunk-YFNRDXUY.js';
5
5
  import { ETYMD_DIR } from './chunk-JHZ2BN4U.js';
6
6
  import './chunk-LSZGCKIQ.js';
7
7
  import './chunk-4VPBP6K6.js';
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { VERSION } from './chunk-J7XVOTZL.js';
2
+ import { VERSION } from './chunk-YFNRDXUY.js';
3
3
  import { PACK_VERSION } from './chunk-LSZGCKIQ.js';
4
4
  import { normalizeRelPath, isDirectory, readJson, git, matchesAnyGlob, pathExists, readText } from './chunk-4VPBP6K6.js';
5
5
  import path from 'path';
@@ -2,7 +2,7 @@
2
2
  import { measureContext } from './chunk-F75Q43BC.js';
3
3
  import { readLedger, reconcileLedger, writeLedger, visibleFindings } from './chunk-3E2IPCRY.js';
4
4
  import { DEFAULT_CONFIG, readConfig, CONFIG_FILE } from './chunk-DWL2IKZH.js';
5
- import { scanProject, expandFileGlobs } from './chunk-5GQ52DK6.js';
5
+ import { scanProject, expandFileGlobs } from './chunk-MTVEJ33D.js';
6
6
  import { ETYMD_DIR, writeCachedFacts, readBaseline, deriveProfile, baselineCarriesMachinePath, BASELINE_FILE } from './chunk-JHZ2BN4U.js';
7
7
  import { PACK_VERSION } from './chunk-LSZGCKIQ.js';
8
8
  import { pathExists, readJson, readText, isDirectory, matchesAnyGlob, git, normalizeRelPath, isCiEnvironment, isExecutable } from './chunk-4VPBP6K6.js';
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { parseFailOnTier, runAudit, meetsFailOn } from './chunk-JVLWM27I.js';
2
+ import { parseFailOnTier, runAudit, meetsFailOn } from './chunk-O3PHW4OP.js';
3
3
  import { print, section, theme, renderLensCoverage, renderFindings, renderLedgerDiff } from './chunk-5BVKFJWM.js';
4
4
 
5
5
  // 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.11.0"};
4
+ version: "0.12.0"};
5
5
 
6
6
  // src/version.ts
7
7
  var VERSION = package_default.version;
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { VERSION } from './chunk-J7XVOTZL.js';
2
+ import { VERSION } from './chunk-YFNRDXUY.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-A75FVXHD.js');
34
+ const { run } = await import('./audit-BXHJUTMJ.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-QLGIYFCX.js');
47
+ const { run } = await import('./init-EKQ33WYS.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-OUJ4UPGK.js');
55
+ const { run } = await import('./approve-PP2QJQ3Y.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-VWEXWM6G.js');
61
+ const { run } = await import('./scan-XS4JPS7J.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-OZQTOUFR.js');
67
+ const { run } = await import('./doctor-7SLSUSVK.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-MYUESUMM.js');
79
+ const { run } = await import('./brief-GDLJEG6X.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-Z7W3QBEV.js');
114
+ const { sweep } = await import('./fleet-5NVOUIZD.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-Z7W3QBEV.js');
129
+ const { check } = await import('./fleet-5NVOUIZD.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-Z7W3QBEV.js');
139
+ const { add } = await import('./fleet-5NVOUIZD.js');
140
140
  const shared = cmd.optsWithGlobals();
141
141
  await add({
142
142
  cwd: resolveCwd(cmd),
@@ -159,7 +159,7 @@ fleet.command("dismiss").argument("<name>", "the registered project name").argum
159
159
  "Dismiss a project's finding from any cwd \u2014 corp ledgers persist beside the manifest"
160
160
  ).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(
161
161
  (name, id, _opts, cmd) => action(async () => {
162
- const { dismiss } = await import('./fleet-Z7W3QBEV.js');
162
+ const { dismiss } = await import('./fleet-5NVOUIZD.js');
163
163
  const opts = cmd.optsWithGlobals();
164
164
  await dismiss({
165
165
  cwd: resolveCwd(cmd),
@@ -174,29 +174,33 @@ fleet.command("accept").argument("<name>", "the registered project name").argume
174
174
  "Accept a project's finding as a known trade-off \u2014 corp ledgers persist beside the manifest"
175
175
  ).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(
176
176
  (name, id, _opts, cmd) => action(async () => {
177
- const { accept } = await import('./fleet-Z7W3QBEV.js');
177
+ const { accept } = await import('./fleet-5NVOUIZD.js');
178
178
  const opts = cmd.optsWithGlobals();
179
179
  await accept({ cwd: resolveCwd(cmd), manifest: opts.manifest, name, id, reason: opts.reason });
180
180
  })
181
181
  );
182
182
  program.command("screen").description(
183
183
  "Content screen: check for text that must never be published. Bring your own patterns \u2014 etymd ships none."
184
- ).option("--staged", "screen what a commit is about to add (default)").option("--tree", "screen every tracked file \u2014 what is about to leave the machine").option("--message <file>", "screen a commit message (the commit-msg hook's $1)").option("--dir <dir>", "screen an unpacked build artifact \u2014 what actually ships").option("--patterns <file>", "pattern file (default: ~/.config/etymd/screen-patterns)").option("--advisory", "report without failing").action(
184
+ ).option("--staged", "screen what a commit is about to add (default)").option("--tree", "screen every tracked file \u2014 what is about to leave the machine").option("--message <file>", "screen a commit message (the commit-msg hook's $1)").option("--dir <dir>", "screen an unpacked build artifact \u2014 what actually ships").option("--patterns <file>", "pattern file (default: ~/.config/etymd/screen-patterns)").option("--advisory", "report without failing").option(
185
+ "--upstream <remote>",
186
+ "fork's upstream remote: files byte-identical to its refs skip vocabulary-class patterns (secret patterns stay absolute)"
187
+ ).action(
185
188
  (opts, cmd) => action(async () => {
186
- const { run } = await import('./screen-NSWNC7VQ.js');
189
+ const { run } = await import('./screen-UF7VZ26C.js');
187
190
  const scope = opts.message ? "message" : opts.dir ? "dir" : opts.tree ? "tree" : "staged";
188
191
  await run({
189
192
  cwd: resolveCwd(cmd),
190
193
  scope,
191
194
  target: opts.message ?? opts.dir,
192
195
  patterns: opts.patterns,
193
- advisory: opts.advisory
196
+ advisory: opts.advisory,
197
+ upstream: opts.upstream
194
198
  });
195
199
  })
196
200
  );
197
201
  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(
198
202
  (opts, cmd) => action(async () => {
199
- const { run } = await import('./gates-47YTMMLZ.js');
203
+ const { run } = await import('./gates-OS5V3P4C.js');
200
204
  await run({ cwd: resolveCwd(cmd), ci: opts.ci, yes: opts.yes });
201
205
  })
202
206
  );
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { run } from './chunk-2KVQBL4E.js';
3
- import './chunk-JVLWM27I.js';
2
+ import { run } from './chunk-SWA4NQHB.js';
3
+ import './chunk-O3PHW4OP.js';
4
4
  import './chunk-F75Q43BC.js';
5
5
  import './chunk-3E2IPCRY.js';
6
6
  import './chunk-DWL2IKZH.js';
7
7
  import './chunk-5BVKFJWM.js';
8
- import './chunk-5GQ52DK6.js';
9
- import './chunk-J7XVOTZL.js';
8
+ import './chunk-MTVEJ33D.js';
9
+ import './chunk-YFNRDXUY.js';
10
10
  import './chunk-JHZ2BN4U.js';
11
11
  import './chunk-LSZGCKIQ.js';
12
12
  import './chunk-4VPBP6K6.js';
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { dismiss, accept } from './chunk-2VLNI3L2.js';
3
- import { parseFailOnTier, meetsFailOn, runAudit } from './chunk-JVLWM27I.js';
3
+ import { parseFailOnTier, meetsFailOn, runAudit } from './chunk-O3PHW4OP.js';
4
4
  import './chunk-F75Q43BC.js';
5
5
  import { readLedger } from './chunk-3E2IPCRY.js';
6
6
  import { DEFAULT_CONFIG } from './chunk-DWL2IKZH.js';
7
7
  import { print, theme, section, renderFleetRows, renderFleetNotes, renderFindings, TIER_BADGE, glyph } from './chunk-5BVKFJWM.js';
8
- import { scanProject } from './chunk-5GQ52DK6.js';
9
- import './chunk-J7XVOTZL.js';
8
+ import { scanProject } from './chunk-MTVEJ33D.js';
9
+ import './chunk-YFNRDXUY.js';
10
10
  import { ETYMD_DIR } from './chunk-JHZ2BN4U.js';
11
11
  import './chunk-LSZGCKIQ.js';
12
12
  import { readText, pathExists, git, isDirectory } from './chunk-4VPBP6K6.js';
@@ -688,7 +688,7 @@ async function checkCorpEmails(manifest, findings, disclosures) {
688
688
  }
689
689
  async function checkGateDrift(manifest, findings, disclosures) {
690
690
  const { planWorkflow } = await import('./generate-PMCP37DC.js');
691
- const { scanProject: scanProject2 } = await import('./scan-W7RHZVSF.js');
691
+ const { scanProject: scanProject2 } = await import('./scan-N4BKVL5F.js');
692
692
  const { readConfig } = await import('./config-XAH6PA5G.js');
693
693
  for (const entry of manifest.entries) {
694
694
  const root = entry.resolvedRoot;
@@ -3,8 +3,8 @@ import { applyFiles } from './chunk-GWGKEPRX.js';
3
3
  import { deriveFailOn, derivedCommands, planWorkflow, runPrefix, isSafeGateCommand } from './chunk-HRJJQCMT.js';
4
4
  import { readConfig, CONFIG_FILE, configPath } from './chunk-DWL2IKZH.js';
5
5
  import { section, print, glyph, theme, renderPlan } from './chunk-5BVKFJWM.js';
6
- import { scanProject } from './chunk-5GQ52DK6.js';
7
- import './chunk-J7XVOTZL.js';
6
+ import { scanProject } from './chunk-MTVEJ33D.js';
7
+ import './chunk-YFNRDXUY.js';
8
8
  import './chunk-JHZ2BN4U.js';
9
9
  import './chunk-LSZGCKIQ.js';
10
10
  import { readText, git } from './chunk-4VPBP6K6.js';
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.11.0",
33
+ version: "0.12.0",
34
34
  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.",
35
35
  keywords: [
36
36
  "cli",
@@ -3,8 +3,8 @@ import { applyFiles } from './chunk-GWGKEPRX.js';
3
3
  import { planWorkflow } from './chunk-HRJJQCMT.js';
4
4
  import './chunk-DWL2IKZH.js';
5
5
  import { theme, renderFacts, print, renderPlan, section, glyph } from './chunk-5BVKFJWM.js';
6
- import { scanProject } from './chunk-5GQ52DK6.js';
7
- import { VERSION } from './chunk-J7XVOTZL.js';
6
+ import { scanProject } from './chunk-MTVEJ33D.js';
7
+ import { VERSION } from './chunk-YFNRDXUY.js';
8
8
  import { writeCachedFacts, deriveProfile, writeBaseline, CACHE_DIR } from './chunk-JHZ2BN4U.js';
9
9
  import { PACK_VERSION } from './chunk-LSZGCKIQ.js';
10
10
  import { git, readText } from './chunk-4VPBP6K6.js';
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ export { scanProject } from './chunk-MTVEJ33D.js';
3
+ import './chunk-YFNRDXUY.js';
4
+ import './chunk-LSZGCKIQ.js';
5
+ import './chunk-4VPBP6K6.js';
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { print, renderFacts, theme } from './chunk-5BVKFJWM.js';
3
- import { scanProject } from './chunk-5GQ52DK6.js';
4
- import './chunk-J7XVOTZL.js';
3
+ import { scanProject } from './chunk-MTVEJ33D.js';
4
+ import './chunk-YFNRDXUY.js';
5
5
  import { writeCachedFacts } from './chunk-JHZ2BN4U.js';
6
6
  import './chunk-LSZGCKIQ.js';
7
7
  import './chunk-4VPBP6K6.js';
@@ -4,6 +4,7 @@ import { git, readText, pathExists } from './chunk-4VPBP6K6.js';
4
4
  import { promises } from 'fs';
5
5
  import path from 'path';
6
6
 
7
+ var CLASS_DIRECTIVE = /^#[ \t]*class:[ \t]*(secret|vocabulary)\b/i;
7
8
  var MACHINE_PATH_RE = /\/(?:Users|home)\/[A-Za-z0-9._-]+\//;
8
9
  var ALLOW_MARKER = "allow-published-string";
9
10
  var ALLOW_FILE = ".etymd-screen-allow";
@@ -48,14 +49,28 @@ function makeAllowEntry(patternStr) {
48
49
  };
49
50
  }
50
51
  }
52
+ function compileOnePattern(l) {
53
+ try {
54
+ return new RegExp(l, "i");
55
+ } catch {
56
+ return new RegExp(l.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i");
57
+ }
58
+ }
51
59
  function compilePatterns(raw) {
52
- return raw.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#")).map((l) => {
53
- try {
54
- return new RegExp(l, "i");
55
- } catch {
56
- return new RegExp(l.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i");
60
+ const out = [];
61
+ let cls = "secret";
62
+ for (const rawLine of raw.split("\n")) {
63
+ const l = rawLine.trim();
64
+ if (!l) continue;
65
+ const dir = CLASS_DIRECTIVE.exec(l);
66
+ if (dir) {
67
+ cls = dir[1].toLowerCase();
68
+ continue;
57
69
  }
58
- });
70
+ if (l.startsWith("#")) continue;
71
+ out.push({ re: compileOnePattern(l), cls });
72
+ }
73
+ return out;
59
74
  }
60
75
  var META = ".[]*^$+?(){}|";
61
76
  function patternLiteral(source) {
@@ -101,13 +116,17 @@ async function selfIdentities(cwd) {
101
116
  names.delete("");
102
117
  return [...names];
103
118
  }
104
- function screenText(text, file, patterns, allow = []) {
119
+ function screenText(text, file, patterns, allow = [], skipVocabulary = false) {
120
+ const classed = patterns.map(
121
+ (p) => p instanceof RegExp ? { re: p, cls: "secret" } : p
122
+ );
123
+ const active = skipVocabulary ? classed.filter((p) => p.cls === "secret") : classed;
105
124
  const hits = [];
106
125
  const lines = text.split("\n");
107
126
  for (const [i, line] of lines.entries()) {
108
127
  if (line.includes(ALLOW_MARKER)) continue;
109
128
  if (allow.some((entry) => entry.pattern.test(line))) continue;
110
- for (const re of patterns) {
129
+ for (const { re } of active) {
111
130
  if (re.test(line)) {
112
131
  hits.push({ file, line: i + 1, text: line.trim().slice(0, 160), reason: String(re) });
113
132
  break;
@@ -124,6 +143,30 @@ function screenText(text, file, patterns, allow = []) {
124
143
  }
125
144
  return hits;
126
145
  }
146
+ async function collectUpstreamBlobs(cwd, remote) {
147
+ const refsRaw = await git(cwd, ["for-each-ref", "--format=%(refname)", `refs/remotes/${remote}/`]);
148
+ const refs = (refsRaw ?? "").split("\n").filter(Boolean);
149
+ if (!refs.length) return null;
150
+ const blobs = /* @__PURE__ */ new Set();
151
+ for (const ref of refs) {
152
+ const tree = await git(cwd, ["ls-tree", "-r", ref]);
153
+ if (tree === null) continue;
154
+ for (const line of tree.split("\n")) {
155
+ const m = /^\d+ blob ([0-9a-f]+)\t/.exec(line);
156
+ if (m) blobs.add(m[1]);
157
+ }
158
+ }
159
+ return blobs.size ? blobs : null;
160
+ }
161
+ async function stagedBlobShas(cwd) {
162
+ const raw = await git(cwd, ["ls-files", "-s"]);
163
+ const map = /* @__PURE__ */ new Map();
164
+ for (const line of (raw ?? "").split("\n")) {
165
+ const m = /^\d+ ([0-9a-f]+) \d+\t(.+)$/.exec(line);
166
+ if (m) map.set(m[2], m[1]);
167
+ }
168
+ return map;
169
+ }
127
170
  async function walk(dir, out, depth = 0) {
128
171
  if (depth > 12) return;
129
172
  let entries;
@@ -189,7 +232,25 @@ async function run(opts) {
189
232
  return true;
190
233
  });
191
234
  const allow = validatedEntries;
192
- const active = selves.length ? patterns.filter((re) => !isSelfName(re, selves)) : patterns;
235
+ const active = selves.length ? patterns.filter((p) => !isSelfName(p.re, selves)) : patterns;
236
+ const hasVocab = active.some((p) => p.cls === "vocabulary");
237
+ const upstreamRemote = hasVocab && (opts.scope === "staged" || opts.scope === "tree") ? opts.upstream ?? await git(opts.cwd, ["config", "--get", "etymd.upstream"]) ?? void 0 : void 0;
238
+ let upstreamBlobs = null;
239
+ let blobByPath = /* @__PURE__ */ new Map();
240
+ let upstreamNote = "";
241
+ if (upstreamRemote) {
242
+ upstreamBlobs = await collectUpstreamBlobs(opts.cwd, upstreamRemote);
243
+ if (upstreamBlobs === null) {
244
+ upstreamNote = `upstream ref '${upstreamRemote}' unavailable \u2014 vocabulary exemption off, screening every pattern`;
245
+ } else {
246
+ blobByPath = await stagedBlobShas(opts.cwd);
247
+ }
248
+ }
249
+ const isUpstreamOwned = (rel) => {
250
+ if (!upstreamBlobs) return false;
251
+ const sha = blobByPath.get(rel);
252
+ return !!sha && upstreamBlobs.has(sha);
253
+ };
193
254
  if (usingLegacy) {
194
255
  print(
195
256
  ` ${glyph.partial} ${theme.warn(`Using legacy ${LEGACY_ALLOW_FILE} file \u2014 rename to ${ALLOW_FILE} and add provenance`)}`
@@ -238,9 +299,12 @@ async function run(opts) {
238
299
  continue;
239
300
  }
240
301
  scanned++;
241
- hits.push(...screenText(raw, rel, active, allow));
302
+ hits.push(...screenText(raw, rel, active, allow, isUpstreamOwned(rel)));
242
303
  }
243
304
  }
305
+ if (upstreamNote) {
306
+ print(` ${glyph.partial} ${theme.warn(upstreamNote)}`);
307
+ }
244
308
  section(
245
309
  `Content screen ${theme.dim(
246
310
  `\xB7 ${opts.scope} \xB7 ${scanned} file(s)${skippedBinary ? ` \xB7 ${skippedBinary} binary skipped` : ""}`
@@ -264,4 +328,4 @@ async function run(opts) {
264
328
  if (!opts.advisory) process.exitCode = 1;
265
329
  }
266
330
 
267
- export { compileAllow, isSelfName, patternLiteral, readScreenable, run, screenText, selfIdentities };
331
+ export { collectUpstreamBlobs, compileAllow, compilePatterns, isSelfName, patternLiteral, readScreenable, run, screenText, selfIdentities, stagedBlobShas };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "etymd",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
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,5 +0,0 @@
1
- #!/usr/bin/env node
2
- export { scanProject } from './chunk-5GQ52DK6.js';
3
- import './chunk-J7XVOTZL.js';
4
- import './chunk-LSZGCKIQ.js';
5
- import './chunk-4VPBP6K6.js';