claude-dev-env 7.4.0 → 7.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3047,9 +3047,10 @@ test('the base install publishes the pstack poteto-mode entry into the managed s
3047
3047
 
3048
3048
  assert.equal(outcome.status, 'installed');
3049
3049
  assert.equal(outcome.warning, null);
3050
- const entryPath = join(fixture.managedRoot, 'skills', 'pstack-poteto-mode');
3051
- assert.equal(lstatSync(entryPath).isSymbolicLink(), true, 'the entry is a pointer into the release');
3052
- assert.equal(existsSync(join(entryPath, 'SKILL.md')), true, 'the pointer resolves to a skill');
3050
+ const pstackRoot = join(fixture.managedRoot, 'skills', 'pstack');
3051
+ const entryPath = join(pstackRoot, 'poteto-mode');
3052
+ assert.equal(lstatSync(pstackRoot).isSymbolicLink(), true, 'the pstack root is a pointer into the release');
3053
+ assert.equal(existsSync(join(entryPath, 'SKILL.md')), true, 'the nested entry resolves to a skill');
3053
3054
  });
3054
3055
 
3055
3056
  test('an unreachable pstack upstream reports a warning and keeps the base install going', t => {
package/bin/pstack.mjs CHANGED
@@ -84,6 +84,17 @@ function adaptSkill(text, name, releaseRoot, skillRoot) {
84
84
  + fork + '\n' + match[2].trimStart();
85
85
  }
86
86
 
87
+ function writePstackPluginManifest(stage, skills) {
88
+ const manifest = {
89
+ $schema: 'https://anthropic.com/claude-code/plugin.schema.json',
90
+ name: 'pstack',
91
+ skills: skills.filter(skill => skill.component === 'pstack').map(skill => `./${skill.slug}`),
92
+ };
93
+ const pluginRoot = join(stage, 'runtime', 'pstack', 'skills');
94
+ mkdirSync(join(pluginRoot, '.claude-plugin'), { recursive: true });
95
+ writeFileSync(join(pluginRoot, '.claude-plugin', 'plugin.json'), json(manifest));
96
+ }
97
+
87
98
  export function prepareRelease(checkout, stage, finalRoot, lock, adapters) {
88
99
  const skills = [];
89
100
  const agents = [];
@@ -102,10 +113,11 @@ export function prepareRelease(checkout, stage, finalRoot, lock, adapters) {
102
113
  }
103
114
  for (const slug of names) {
104
115
  if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) throw new Error(`Invalid skill directory: ${slug}`);
105
- const name = `${component}-${slug}`;
116
+ const name = component === 'pstack' ? `pstack:${slug}` : `${component}-${slug}`;
117
+ const frontmatterName = component === 'pstack' ? slug : name;
106
118
  const path = join('runtime', component, 'skills', slug);
107
119
  const text = readFileSync(join(source, 'skills', slug, 'SKILL.md'), 'utf8');
108
- writeFileSync(join(stage, path, 'SKILL.md'), adaptSkill(text, name, finalRoot, join(finalRoot, path)));
120
+ writeFileSync(join(stage, path, 'SKILL.md'), adaptSkill(text, frontmatterName, finalRoot, join(finalRoot, path)));
109
121
  skills.push({ name, component, slug, path });
110
122
  }
111
123
  const agentHome = join(source, 'agents');
@@ -118,6 +130,7 @@ export function prepareRelease(checkout, stage, finalRoot, lock, adapters) {
118
130
  mkdirSync(join(stage, creatorPath), { recursive: true });
119
131
  writeFileSync(join(stage, creatorPath, 'SKILL.md'), adapters['create-skill.md']);
120
132
  skills.push({ name: 'cde-create-skill', component: 'cde', slug: 'create-skill', path: creatorPath });
133
+ writePstackPluginManifest(stage, skills);
121
134
  const knownNames = new Set(skills.map(skill => `${skill.component}:${skill.slug}`));
122
135
  for (const path of filesUnder(join(stage, 'upstream')).filter(path => path.endsWith('.md') && (path.includes(sep + 'skills' + sep) || path.includes(sep + 'agents' + sep)))) {
123
136
  const references = readFileSync(path, 'utf8').matchAll(/\b(pstack|cursor-team-kit):([a-z][a-z0-9-]*)/g);
@@ -146,7 +159,9 @@ export function verifyRelease(root) {
146
159
  if (digest(readFileSync(assertContained(root, path))) !== hash) throw new Error(`Changed installed file: ${path}`);
147
160
  }
148
161
  for (const skill of release.skills) {
149
- if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(skill.name)) throw new Error('Invalid installed skill name');
162
+ const validPart = value => /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value);
163
+ const expectedName = skill.component === 'pstack' ? `pstack:${skill.slug}` : `${skill.component}-${skill.slug}`;
164
+ if (!validPart(skill.component) || !validPart(skill.slug) || skill.name !== expectedName) throw new Error('Invalid installed skill name');
150
165
  const path = join(assertContained(root, skill.path), 'SKILL.md');
151
166
  if (!Object.hasOwn(release.fileDigests, relative(root, path)) || !existsSync(path)) throw new Error(`Missing entry: ${skill.name}`);
152
167
  }
@@ -177,7 +192,12 @@ export function verifyInstallation(options = {}) {
177
192
 
178
193
  function publish(releaseRoot, release, homes, prior) {
179
194
  const uniqueHomes = [...new Set(homes.map(home => { mkdirSync(home, { recursive: true }); return realpathSync(home); }))];
180
- const links = Object.fromEntries(uniqueHomes.flatMap(home => release.skills.map(skill => [join(home, skill.name), join(releaseRoot, skill.path)])));
195
+ const pstackSkill = release.skills.find(skill => skill.component === 'pstack');
196
+ const directSkills = release.skills.filter(skill => skill.component !== 'pstack');
197
+ const links = Object.fromEntries(uniqueHomes.flatMap(home => [
198
+ ...(pstackSkill ? [[join(home, 'pstack'), join(releaseRoot, dirname(pstackSkill.path))]] : []),
199
+ ...directSkills.map(skill => [join(home, skill.name), join(releaseRoot, skill.path)]),
200
+ ]));
181
201
  const paths = new Set([...Object.keys(prior?.links ?? {}), ...Object.keys(links)]);
182
202
  const before = new Map();
183
203
  for (const path of paths) {
@@ -44,15 +44,23 @@ for (const host of ['claude', 'codex', 'cursor']) {
44
44
  assert.equal(result.commit, '27e2a62ff94f9af4b5e68435e41cdceacadb840c');
45
45
  assert.equal(result.verification, 'filesystem-only');
46
46
  const release = join(f.store, 'releases', result.release);
47
- const expected = ['cde-create-skill', 'cursor-team-kit-control-cli', 'cursor-team-kit-control-ui', 'cursor-team-kit-deslop', 'pstack-poteto-mode'];
47
+ const releaseManifest = JSON.parse(readFileSync(join(release, 'release.json'), 'utf8'));
48
+ assert.equal(releaseManifest.skills.find(skill => skill.component === 'pstack').name, 'pstack:poteto-mode');
49
+ const expected = ['cde-create-skill', 'cursor-team-kit-control-cli', 'cursor-team-kit-control-ui', 'cursor-team-kit-deslop', 'pstack'];
48
50
  for (const home of ['.claude', '.agents']) {
49
- assert.deepEqual(readdirSync(join(f.options.project, home, 'skills')).sort(), expected);
50
- const entry = join(f.options.project, home, 'skills', 'pstack-poteto-mode');
51
+ const skillsHome = join(f.options.project, home, 'skills');
52
+ assert.deepEqual(readdirSync(skillsHome).sort(), expected);
53
+ const pstackRoot = join(skillsHome, 'pstack');
54
+ assert.equal(lstatSync(pstackRoot).isSymbolicLink(), true);
55
+ const entry = join(pstackRoot, 'poteto-mode');
51
56
  assert.equal(realpathSync(entry), join(release, 'runtime', 'pstack', 'skills', 'poteto-mode'));
52
57
  const text = readFileSync(join(entry, 'SKILL.md'), 'utf8');
53
- assert.match(text, /name: pstack-poteto-mode/);
58
+ assert.match(text, /name: poteto-mode/);
54
59
  assert.ok(text.includes(JSON.stringify(join(release, 'compat', 'common.md'))));
55
60
  assert.equal(readFileSync(join(entry, 'playbooks', 'feature.md'), 'utf8'), 'Build a small task, delegate, and verify it.\n');
61
+ const manifest = JSON.parse(readFileSync(join(pstackRoot, '.claude-plugin', 'plugin.json'), 'utf8'));
62
+ assert.equal(manifest.name, 'pstack');
63
+ assert.deepEqual(manifest.skills, ['./poteto-mode']);
56
64
  }
57
65
  assert.equal(existsSync(join(f.options.project, '.cursor')), false);
58
66
  for (const component of baseLock.components) {
@@ -89,10 +97,12 @@ test('changed, added, and removed skills converge while old releases remain read
89
97
  const after = installPstack({ ...f.options, lock: { ...baseLock, commit: 'b'.repeat(40) } }, f.dependencies);
90
98
  assert.equal(after.skillCount, 6);
91
99
  for (const home of ['.claude', '.agents']) {
92
- const path = join(f.options.project, home, 'skills');
93
- assert.equal(existsSync(join(path, 'pstack-old')), false);
94
- assert.equal(readFileSync(join(path, 'pstack-new', 'SKILL.md'), 'utf8').endsWith('New body.\n'), true);
95
- assert.equal(readFileSync(join(path, 'pstack-poteto-mode', 'playbooks', 'feature.md'), 'utf8'), 'Changed playbook.\n');
100
+ const pstackRoot = join(f.options.project, home, 'skills', 'pstack');
101
+ assert.equal(existsSync(join(pstackRoot, 'old')), false);
102
+ assert.equal(readFileSync(join(pstackRoot, 'new', 'SKILL.md'), 'utf8').endsWith('New body.\n'), true);
103
+ assert.equal(readFileSync(join(pstackRoot, 'poteto-mode', 'playbooks', 'feature.md'), 'utf8'), 'Changed playbook.\n');
104
+ const manifest = JSON.parse(readFileSync(join(pstackRoot, '.claude-plugin', 'plugin.json'), 'utf8'));
105
+ assert.deepEqual(manifest.skills, ['./new', './poteto-mode']);
96
106
  }
97
107
  assert.equal(readFileSync(join(oldRelease, 'runtime', 'pstack', 'skills', 'poteto-mode', 'SKILL.md'), 'utf8'), firstBody);
98
108
  });
@@ -117,9 +127,9 @@ test('a failed first install returns failure and publishes no skills', t => {
117
127
  assert.equal(existsSync(join(f.store, '.install-lock')), false);
118
128
  });
119
129
 
120
- test('unmanaged skill collision preserves user files', t => {
130
+ test('unmanaged pstack root collision preserves user files', t => {
121
131
  const f = fixture(t);
122
- const userFile = join(f.options.project, '.claude', 'skills', 'pstack-poteto-mode', 'SKILL.md');
132
+ const userFile = join(f.options.project, '.claude', 'skills', 'pstack', 'poteto-mode', 'SKILL.md');
123
133
  put(userFile, 'Personal workflow\n');
124
134
  assert.throws(() => installPstack(f.options, f.dependencies), /Keep existing unmanaged path/);
125
135
  assert.equal(readFileSync(userFile, 'utf8'), 'Personal workflow\n');
@@ -25,22 +25,23 @@ _hooks_dir = str(Path(__file__).resolve().parent.parent)
25
25
  if _hooks_dir not in sys.path:
26
26
  sys.path.insert(0, _hooks_dir)
27
27
 
28
- from hooks_constants.env_var_table_code_drift_constants import ( # noqa: E402
29
- ALL_CODE_FILE_EXTENSIONS,
28
+ from env_var_table_rows import (
29
+ _code_file_reference_in_cell,
30
+ _env_var_name_in_cell,
31
+ _is_separator_row,
32
+ _row_cells,
33
+ iter_env_var_table_rows,
34
+ )
35
+ from hooks_constants.env_var_table_code_drift_constants import (
30
36
  ALL_NOISE_DIRECTORY_NAMES,
31
- BACKTICK_TOKEN_PATTERN,
32
- CODE_FENCE_PATTERN,
33
37
  DRIFT_ADDITIONAL_CONTEXT,
34
38
  DRIFT_MESSAGE_TEMPLATE,
35
39
  DRIFT_SYSTEM_MESSAGE,
36
- ENV_VAR_NAME_PATTERN,
37
40
  GIT_DIRECTORY_NAME,
38
41
  MARKDOWN_FILE_EXTENSION,
39
42
  MAX_DRIFT_ISSUES,
40
43
  MAX_SUBTREE_FILES_SCANNED,
41
44
  MINIMUM_ENV_VAR_ROW_CELL_COUNT,
42
- SEPARATOR_CELL_PATTERN,
43
- TABLE_ROW_PATTERN,
44
45
  )
45
46
  from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
46
47
  from hooks_constants.multi_edit_reconstruction import ( # noqa: E402
@@ -65,87 +66,6 @@ def is_markdown_file(file_path: str) -> bool:
65
66
  return extension.lower() == MARKDOWN_FILE_EXTENSION
66
67
 
67
68
 
68
- def _row_cells(table_line: str) -> list[str]:
69
- """Return the trimmed cells of one markdown table row.
70
-
71
- Args:
72
- table_line: A single line that begins with a pipe character.
73
-
74
- Returns:
75
- The text of each pipe-delimited cell, stripped, with the empty leading
76
- and trailing segments a bounding pipe produces removed.
77
- """
78
- stripped_line = table_line.strip()
79
- inner = stripped_line.strip("|")
80
- return [each_cell.strip() for each_cell in inner.split("|")]
81
-
82
-
83
- def _first_backtick_token(cell_text: str) -> str | None:
84
- """Return the first backtick-wrapped token in a cell, when it has one.
85
-
86
- Args:
87
- cell_text: The trimmed text of a table cell.
88
-
89
- Returns:
90
- The text inside the first pair of backticks, or None when the cell
91
- carries no backtick-wrapped token.
92
- """
93
- token_match = BACKTICK_TOKEN_PATTERN.search(cell_text)
94
- if token_match is None:
95
- return None
96
- inner_text = token_match.group(1).strip()
97
- return inner_text or None
98
-
99
-
100
- def _env_var_name_in_cell(cell_text: str) -> str | None:
101
- """Return the environment-variable name a cell names, when it names one.
102
-
103
- Args:
104
- cell_text: The trimmed text of a table cell.
105
-
106
- Returns:
107
- The UPPER_SNAKE variable name inside the first backticks, or None when
108
- the cell names no variable-shaped token.
109
- """
110
- token = _first_backtick_token(cell_text)
111
- if token is None:
112
- return None
113
- if ENV_VAR_NAME_PATTERN.match(token) is None:
114
- return None
115
- return token
116
-
117
-
118
- def _code_file_reference_in_cell(cell_text: str) -> str | None:
119
- """Return the code-file path a cell names, when it names one.
120
-
121
- Args:
122
- cell_text: The trimmed text of a table cell.
123
-
124
- Returns:
125
- The relative code-file path inside the first backticks, or None when the
126
- token carries no recognized code-file extension.
127
- """
128
- token = _first_backtick_token(cell_text)
129
- if token is None:
130
- return None
131
- _, extension = os.path.splitext(token)
132
- if extension.lower() not in ALL_CODE_FILE_EXTENSIONS:
133
- return None
134
- return token
135
-
136
-
137
- def _is_separator_row(all_cells: list[str]) -> bool:
138
- """Return whether every cell is a markdown table header-separator cell.
139
-
140
- Args:
141
- all_cells: The trimmed cells of one table row.
142
-
143
- Returns:
144
- True when each cell holds only dashes, colons, and whitespace.
145
- """
146
- return all(SEPARATOR_CELL_PATTERN.match(each_cell) is not None for each_cell in all_cells)
147
-
148
-
149
69
  def _resolve_scan_root(markdown_directory: Path) -> Path:
150
70
  """Return the repository directory whose subtree bounds the code-file search.
151
71
 
@@ -248,8 +168,8 @@ def _code_file_reads_variable(code_file: Path, variable_name: str) -> bool | Non
248
168
  def find_drift_rows(content: str, markdown_directory: Path) -> list[str]:
249
169
  """Return each env-var table row whose code file does not read the variable.
250
170
 
251
- Walks the markdown content line by line, skipping lines inside a fenced code
252
- block. A table row counts when its first cell names an UPPER_SNAKE variable and
171
+ Reads environment-variable tables and headerless edit fragments outside code
172
+ fences. A row counts when its first cell names an UPPER_SNAKE variable and
253
173
  a later cell names a code file with a recognized extension. The row drifts when
254
174
  that code file resolves under the scan root yet its source never references the
255
175
  variable name; a row whose code file does not resolve, or cannot be read, is
@@ -266,15 +186,7 @@ def find_drift_rows(content: str, markdown_directory: Path) -> list[str]:
266
186
  scan_root = _resolve_scan_root(markdown_directory)
267
187
  drift_rows: list[str] = []
268
188
  already_reported: set[str] = set()
269
- is_inside_code_fence = False
270
- for each_line in content.splitlines():
271
- if CODE_FENCE_PATTERN.match(each_line) is not None:
272
- is_inside_code_fence = not is_inside_code_fence
273
- continue
274
- if is_inside_code_fence:
275
- continue
276
- if TABLE_ROW_PATTERN.match(each_line) is None:
277
- continue
189
+ for each_line in iter_env_var_table_rows(content):
278
190
  each_finding = _drift_finding_for_row(each_line, scan_root)
279
191
  if each_finding is None or each_finding in already_reported:
280
192
  continue
@@ -0,0 +1,130 @@
1
+ """Recognize Markdown environment-variable tables and parse their cells."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from collections.abc import Iterator
7
+
8
+ from hooks_constants.env_var_table_code_drift_constants import (
9
+ ALL_CODE_FILE_EXTENSIONS,
10
+ ALL_GENERIC_ENV_VAR_HEADERS,
11
+ BACKTICK_TOKEN_PATTERN,
12
+ CODE_FENCE_PATTERN,
13
+ ENV_VAR_HEADER_PATTERN,
14
+ ENV_VAR_HEADING_PATTERN,
15
+ ENV_VAR_NAME_PATTERN,
16
+ SEPARATOR_CELL_PATTERN,
17
+ TABLE_ROW_PATTERN,
18
+ )
19
+
20
+
21
+ def _row_cells(table_line: str) -> list[str]:
22
+ """Return the trimmed cells of one markdown table row.
23
+
24
+ Args:
25
+ table_line: A single line that begins with a pipe character.
26
+
27
+ Returns:
28
+ The text of each pipe-delimited cell, stripped, with the empty leading
29
+ and trailing segments a bounding pipe produces removed.
30
+ """
31
+ stripped_line = table_line.strip()
32
+ inner = stripped_line.strip("|")
33
+ return [each_cell.strip() for each_cell in inner.split("|")]
34
+
35
+
36
+ def _first_backtick_token(cell_text: str) -> str | None:
37
+ """Return the first backtick-wrapped token in a cell, when it has one.
38
+
39
+ Args:
40
+ cell_text: The trimmed text of a table cell.
41
+
42
+ Returns:
43
+ The text inside the first pair of backticks, or None when the cell
44
+ carries no backtick-wrapped token.
45
+ """
46
+ token_match = BACKTICK_TOKEN_PATTERN.search(cell_text)
47
+ if token_match is None:
48
+ return None
49
+ inner_text = token_match.group(1).strip()
50
+ return inner_text or None
51
+
52
+
53
+ def _env_var_name_in_cell(cell_text: str) -> str | None:
54
+ """Return the environment-variable name a cell names, when it names one.
55
+
56
+ Args:
57
+ cell_text: The trimmed text of a table cell.
58
+
59
+ Returns:
60
+ The UPPER_SNAKE variable name inside the first backticks, or None when
61
+ the cell names no variable-shaped token.
62
+ """
63
+ token = _first_backtick_token(cell_text)
64
+ if token is None:
65
+ return None
66
+ if ENV_VAR_NAME_PATTERN.match(token) is None:
67
+ return None
68
+ return token
69
+
70
+
71
+ def _code_file_reference_in_cell(cell_text: str) -> str | None:
72
+ """Return the code-file path a cell names, when it names one.
73
+
74
+ Args:
75
+ cell_text: The trimmed text of a table cell.
76
+
77
+ Returns:
78
+ The relative code-file path inside the first backticks, or None when the
79
+ token carries no recognized code-file extension.
80
+ """
81
+ token = _first_backtick_token(cell_text)
82
+ if token is None:
83
+ return None
84
+ _, extension = os.path.splitext(token)
85
+ if extension.lower() not in ALL_CODE_FILE_EXTENSIONS:
86
+ return None
87
+ return token
88
+
89
+
90
+ def _is_separator_row(all_cells: list[str]) -> bool:
91
+ """Return whether every cell is a markdown table header-separator cell.
92
+
93
+ Args:
94
+ all_cells: The trimmed cells of one table row.
95
+
96
+ Returns:
97
+ True when each cell holds only dashes, colons, and whitespace.
98
+ """
99
+ return all(SEPARATOR_CELL_PATTERN.match(each_cell) is not None for each_cell in all_cells)
100
+
101
+
102
+ def iter_env_var_table_rows(content: str) -> Iterator[str]:
103
+ """Yield environment-table rows and headerless edit fragments outside code fences."""
104
+ is_inside_code_fence = False
105
+ is_environment_table: bool | None = None
106
+ is_environment_section = False
107
+ for each_line in content.splitlines():
108
+ if CODE_FENCE_PATTERN.match(each_line) is not None:
109
+ is_inside_code_fence = not is_inside_code_fence
110
+ is_environment_table = None
111
+ continue
112
+ if is_inside_code_fence:
113
+ continue
114
+ if each_line.lstrip().startswith("#"):
115
+ is_environment_section = ENV_VAR_HEADING_PATTERN.search(each_line) is not None
116
+ if TABLE_ROW_PATTERN.match(each_line) is None:
117
+ is_environment_table = None
118
+ continue
119
+ all_cells = _row_cells(each_line)
120
+ if not all_cells or _is_separator_row(all_cells):
121
+ continue
122
+ if is_environment_table is None:
123
+ header = all_cells[0].strip("`* ")
124
+ is_environment_table = (
125
+ ENV_VAR_HEADER_PATTERN.fullmatch(header) is not None
126
+ or _env_var_name_in_cell(all_cells[0]) is not None
127
+ or (is_environment_section and header.casefold() in ALL_GENERIC_ENV_VAR_HEADERS)
128
+ )
129
+ if is_environment_table:
130
+ yield each_line
@@ -18,10 +18,9 @@ if _hooks_directory not in sys.path:
18
18
  sys.path.insert(0, _hooks_directory)
19
19
 
20
20
  from hooks_constants.local_identity import nas_host # noqa: E402
21
- from hooks_constants.pii_prevention_constants import ( # noqa: E402
21
+ from hooks_constants.pii_prevention_constants import (
22
22
  ALL_ALLOWLISTED_PRIVATE_IP_ADDRESSES,
23
23
  ALL_EXACT_LEGAL_NOTICE_BASENAMES,
24
- ALL_HOME_DIRECTORY_PATH_MARKERS,
25
24
  ALL_PLACEHOLDER_HOME_USERNAMES,
26
25
  ALL_REDACTED_PREVIEW_CATEGORIES,
27
26
  ALL_RFC1918_NETWORK_CIDRS,
@@ -37,6 +36,8 @@ from hooks_constants.pii_prevention_constants import ( # noqa: E402
37
36
  CONFTEST_BASENAME,
38
37
  EMAIL_PATTERN,
39
38
  HOME_PATH_PATTERN,
39
+ HOME_USERNAME_MARKER_PATTERN,
40
+ INTERPOLATED_HOME_USERNAME_PATTERN,
40
41
  IPV4_PATTERN,
41
42
  IPV4_VERSION_NUMBER,
42
43
  MAXIMUM_FINDINGS_PER_SCAN,
@@ -90,9 +91,7 @@ def is_path_exempt_from_pii_scan(file_path: str) -> bool:
90
91
  return False
91
92
  normalized_path = file_path.replace("\\", "/").lower()
92
93
  path_for_suffix_match = (
93
- normalized_path
94
- if normalized_path.startswith("/")
95
- else f"/{normalized_path}"
94
+ normalized_path if normalized_path.startswith("/") else f"/{normalized_path}"
96
95
  )
97
96
  basename = os.path.basename(file_path)
98
97
  basename_lower = basename.lower()
@@ -106,9 +105,7 @@ def is_path_exempt_from_pii_scan(file_path: str) -> bool:
106
105
  or basename_lower.endswith(TEST_MODULE_BASENAME_SUFFIX)
107
106
  ):
108
107
  return True
109
- if TESTS_PATH_SEGMENT in normalized_path or normalized_path.startswith(
110
- TESTS_PATH_PREFIX
111
- ):
108
+ if TESTS_PATH_SEGMENT in normalized_path or normalized_path.startswith(TESTS_PATH_PREFIX):
112
109
  return True
113
110
  if basename_lower.endswith(ALL_SOURCE_TEST_FILE_SUFFIXES) and (
114
111
  SPEC_BASENAME_MARKER in basename_lower or TEST_BASENAME_MARKER in basename_lower
@@ -130,11 +127,7 @@ def _redact_sensitive_preview(matched_text: str) -> str:
130
127
  return REDACTED_SHORT_PREVIEW
131
128
  prefix_length = REDACTED_PREVIEW_PREFIX_LENGTH
132
129
  suffix_length = REDACTED_PREVIEW_SUFFIX_LENGTH
133
- return (
134
- matched_text[:prefix_length]
135
- + REDACTED_PREVIEW_ELLIPSIS
136
- + matched_text[-suffix_length:]
137
- )
130
+ return matched_text[:prefix_length] + REDACTED_PREVIEW_ELLIPSIS + matched_text[-suffix_length:]
138
131
 
139
132
 
140
133
  def _build_preview(matched_text: str, category: str) -> str:
@@ -161,8 +154,7 @@ def _is_safe_email_domain(domain_name: str) -> bool:
161
154
  if domain_name in ALL_SAFE_EMAIL_DOMAINS:
162
155
  return True
163
156
  return any(
164
- domain_name.endswith("." + each_safe_domain)
165
- for each_safe_domain in ALL_SAFE_EMAIL_DOMAINS
157
+ domain_name.endswith("." + each_safe_domain) for each_safe_domain in ALL_SAFE_EMAIL_DOMAINS
166
158
  )
167
159
 
168
160
 
@@ -180,8 +172,8 @@ def _home_username_offset(matched_path: str) -> tuple[int, str] | None:
180
172
 
181
173
  C:\\Users\\Users\\file.txt -> (9, "Users") the second Users, not the first
182
174
 
183
- Replacing backslashes with forward slashes preserves length, so the marker
184
- offset computed on the normalized path indexes *matched_path* directly.
175
+ The marker match consumes repeated separators, so the username starts after
176
+ both ordinary and escaped Windows path separators.
185
177
 
186
178
  Args:
187
179
  matched_path: The matched home-path substring.
@@ -189,18 +181,14 @@ def _home_username_offset(matched_path: str) -> tuple[int, str] | None:
189
181
  Returns:
190
182
  The username's start index and value, or None when no marker is found.
191
183
  """
192
- normalized_path = matched_path.replace("\\", "/")
193
- lowered_path = normalized_path.lower()
194
- for each_marker in ALL_HOME_DIRECTORY_PATH_MARKERS:
195
- marker_index = lowered_path.find(each_marker)
196
- if marker_index < 0:
197
- continue
198
- username_start = marker_index + len(each_marker)
199
- username = normalized_path[username_start:].split("/", 1)[0]
200
- if not username:
201
- return None
202
- return username_start, username
203
- return None
184
+ marker = HOME_USERNAME_MARKER_PATTERN.search(matched_path)
185
+ if marker is None:
186
+ return None
187
+ username_start = marker.end()
188
+ username = matched_path[username_start:].replace("\\", "/").split("/", 1)[0]
189
+ if not username:
190
+ return None
191
+ return username_start, username
204
192
 
205
193
 
206
194
  def _username_from_home_path(matched_path: str) -> str | None:
@@ -226,6 +214,8 @@ def _redact_home_path_username(matched_text: str) -> str:
226
214
 
227
215
 
228
216
  def _is_placeholder_home_username(username: str) -> bool:
217
+ if INTERPOLATED_HOME_USERNAME_PATTERN.fullmatch(username) is not None:
218
+ return True
229
219
  if ANGLE_BRACKET_PLACEHOLDER_PATTERN.match(username) is not None:
230
220
  return True
231
221
  lowered_username = username.lower()
@@ -125,9 +125,10 @@ def test_production_entrypoint_denies_drifted_table(tmp_path: Path) -> None:
125
125
  hook_specific_output = parsed_payload["hookSpecificOutput"]
126
126
  assert completed.returncode == 0, completed.stderr
127
127
  assert hook_specific_output["permissionDecision"] == "deny"
128
- assert "GOOGLE_APPLICATION_CREDENTIALS -> auth/google_auth.py" in hook_specific_output[
129
- "permissionDecisionReason"
130
- ]
128
+ assert (
129
+ "GOOGLE_APPLICATION_CREDENTIALS -> auth/google_auth.py"
130
+ in hook_specific_output["permissionDecisionReason"]
131
+ )
131
132
  assert_hook_deny_log_contains(tmp_path, "env_var_table_code_drift_blocker.py")
132
133
 
133
134
 
@@ -159,3 +160,80 @@ def test_ignores_row_whose_second_cell_is_not_a_code_file(tmp_path: Path) -> Non
159
160
  "| `SOME_FLAG` | `enables the thing` | off |\n"
160
161
  )
161
162
  assert find_drift_rows(content, doc_path.parent) == []
163
+
164
+
165
+ def test_constant_tables_do_not_claim_environment_consumers(tmp_path: Path) -> None:
166
+ _anchor_repo_root(tmp_path)
167
+ _write(tmp_path / "workflow.py", "def run():\n return\n")
168
+ content = (
169
+ "| Symbol | Imported by |\n"
170
+ "| --- | --- |\n"
171
+ "| `PROCESSING_VISIBLE` | `workflow.py` |\n\n"
172
+ "| Component | Location | Purpose |\n"
173
+ "| --- | --- | --- |\n"
174
+ "| `ACCOUNT_LINK_PRIMARY` | `workflow.py` | Selector |\n\n"
175
+ "| Constant | Value | Source |\n"
176
+ "| --- | --- | --- |\n"
177
+ "| `UPLOAD_DELAY_SECONDS` | 5 | `workflow.py` |\n"
178
+ )
179
+ assert find_drift_rows(content, tmp_path) == []
180
+
181
+
182
+ def test_environment_table_after_constants_still_reports_drift(tmp_path: Path) -> None:
183
+ _anchor_repo_root(tmp_path)
184
+ _write(tmp_path / "workflow.py", "def run():\n return\n")
185
+ content = (
186
+ "| Constant | Source |\n"
187
+ "| --- | --- |\n"
188
+ "| `UPLOAD_DELAY_SECONDS` | `workflow.py` |\n\n"
189
+ "| Environment variable | Consumer |\n"
190
+ "| --- | --- |\n"
191
+ "| `UPLOAD_TOKEN` | `workflow.py` |\n"
192
+ )
193
+ assert find_drift_rows(content, tmp_path) == ["UPLOAD_TOKEN -> workflow.py"]
194
+
195
+
196
+ def test_environment_section_supports_name_header(tmp_path: Path) -> None:
197
+ _anchor_repo_root(tmp_path)
198
+ _write(tmp_path / "workflow.py", "def run():\n return\n")
199
+ content = (
200
+ "## Environment variables\n\n"
201
+ "| Name | Consumer |\n"
202
+ "| --- | --- |\n"
203
+ "| `UPLOAD_TOKEN` | `workflow.py` |\n"
204
+ )
205
+ assert find_drift_rows(content, tmp_path) == ["UPLOAD_TOKEN -> workflow.py"]
206
+
207
+
208
+ def test_headerless_edit_fragment_still_reports_drift(tmp_path: Path) -> None:
209
+ _anchor_repo_root(tmp_path)
210
+ _write(tmp_path / "workflow.py", "def run():\n return\n")
211
+ content = "| `UPLOAD_TOKEN` | `workflow.py` |\n"
212
+ assert find_drift_rows(content, tmp_path) == ["UPLOAD_TOKEN -> workflow.py"]
213
+
214
+
215
+ def test_descriptive_environment_header_reports_drift(tmp_path: Path) -> None:
216
+ _anchor_repo_root(tmp_path)
217
+ _write(tmp_path / "workflow.py", "def run():\n return\n")
218
+ content = (
219
+ "| Environment Variable Name | Consumer |\n"
220
+ "| --- | --- |\n"
221
+ "| `UPLOAD_TOKEN` | `workflow.py` |\n"
222
+ )
223
+ assert find_drift_rows(content, tmp_path) == ["UPLOAD_TOKEN -> workflow.py"]
224
+
225
+
226
+ def test_explicit_source_header_overrides_environment_section(tmp_path: Path) -> None:
227
+ _anchor_repo_root(tmp_path)
228
+ _write(tmp_path / "workflow.py", "def run():\n return\n")
229
+ content = (
230
+ "## Environment variables\n\n"
231
+ "| Name | Consumer |\n"
232
+ "| --- | --- |\n"
233
+ "| `UPLOAD_TOKEN` | `workflow.py` |\n\n"
234
+ "The source also defines a delay constant.\n\n"
235
+ "| Constant | Source |\n"
236
+ "| --- | --- |\n"
237
+ "| `UPLOAD_DELAY_SECONDS` | `workflow.py` |\n"
238
+ )
239
+ assert find_drift_rows(content, tmp_path) == ["UPLOAD_TOKEN -> workflow.py"]