instar 1.3.1157 → 1.3.1159

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.
@@ -2,7 +2,7 @@
2
2
  "schemaVersion": 1,
3
3
  "generatedFrom": "source-tree",
4
4
  "registrySha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
5
- "packageVersion": "1.3.1157",
5
+ "packageVersion": "1.3.1159",
6
6
  "guards": [
7
7
  {
8
8
  "ref": "docs/audits/phase-b/f10-triage.md",
@@ -1,5 +1,5 @@
1
1
  {
2
- "sha256": "94029728ae76297bbe85baa1e37fd5985de80887bb4432afa090612a0a6da69c",
2
+ "sha256": "64b362d2e7aed4d92d9e9948a954ae1c099930efc5972c89842953e81cfd36ff",
3
3
  "registrySha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
4
- "packageVersion": "1.3.1157"
4
+ "packageVersion": "1.3.1159"
5
5
  }
@@ -2,5 +2,5 @@
2
2
  "sha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
3
3
  "articleCount": 88,
4
4
  "generatedFrom": "docs/STANDARDS-REGISTRY.md",
5
- "packageVersion": "1.3.1157"
5
+ "packageVersion": "1.3.1159"
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.1157",
3
+ "version": "1.3.1159",
4
4
  "description": "Coherence infrastructure for self-evolving AI agents — on the Claude Code or Codex subscription you already have.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -11,6 +11,26 @@
11
11
  * Conservative by design: it flags the two concrete shapes we know leak, not
12
12
  * every URL log. The redactUrl module + its tests are exempt.
13
13
  *
14
+ * SCOPE, split by whether the match detects the prohibited FACT or a SPELLING
15
+ * of it (the distinction that decides which half is worth widening):
16
+ *
17
+ * - CREDENTIALED_URL_LITERAL is the FACT. A `user:pass@` inside a URL literal
18
+ * IS the leak, whatever it is called or where it is logged. So resolving a
19
+ * split literal is a real closure, not a bigger net: as of 2026-08-15 the
20
+ * line is scanned with adjacent string concatenations folded, because
21
+ * `"https://user:" + "tok@host"` leaked exactly as much as the one-piece
22
+ * form and was invisible. Folding joins only ADJACENT literals of the same
23
+ * quote style and invents no text.
24
+ *
25
+ * - RISKY_URL_VAR_LOG is a SPELLING. It matches five variable names logged
26
+ * through `console.*`. Renaming the variable (`originUrl`, `endpoint`) or
27
+ * using any other sink (`logger.info`) defeats it — both measured. Growing
28
+ * the name list would make the net finer while leaving the judgment inside
29
+ * the pattern, so it is deliberately NOT widened here. The right repair is
30
+ * to demote it from decider to candidate-gatherer and put the weighing
31
+ * downstream; that changes the check's authority and belongs in a spec, not
32
+ * in a regex edit.
33
+ *
14
34
  * Exit 0 = clean. Exit 1 = at least one offending site (printed).
15
35
  */
16
36
 
@@ -29,6 +49,23 @@ const EXEMPT = [
29
49
  /** A literal `scheme://user:pass@` in a string that is being logged. */
30
50
  const CREDENTIALED_URL_LITERAL = /['"`][a-z][a-z0-9+.-]*:\/\/[^/@'"`\s]+:[^/@'"`\s]+@/i;
31
51
 
52
+ /**
53
+ * Fold `"a" + "b"` (adjacent string literals, same quote style) into `"ab"` so a
54
+ * credentialed URL split across a concatenation is scanned as the string it
55
+ * actually builds. Only literal+literal joins are folded — a variable operand
56
+ * ends the fold, so nothing is invented and no non-literal is assumed.
57
+ */
58
+ export function collapseConcatenation(line) {
59
+ let out = line;
60
+ for (let i = 0; i < 8; i += 1) {
61
+ const next = out.replace(/(['"])((?:\\.|(?!\1)[^\\])*)\1\s*\+\s*(['"])((?:\\.|(?!\3)[^\\])*)\3/g,
62
+ (_m, q1, a, _q2, b) => `${q1}${a}${b}${q1}`);
63
+ if (next === out) break;
64
+ out = next;
65
+ }
66
+ return out;
67
+ }
68
+
32
69
  /** console.* logging a variable named like a clone/remote URL without redactUrl on the same line. */
33
70
  const RISKY_URL_VAR_LOG = /console\.(log|error|warn|info)\([^)]*\b(repoUrl|cloneUrl|remoteUrl|pushUrl|gitUrl)\b/;
34
71
 
@@ -45,14 +82,32 @@ function walk(dir, out = []) {
45
82
  return out;
46
83
  }
47
84
 
85
+ /**
86
+ * The scan, callable. Returns the offender list instead of exiting, so the
87
+ * behaviour can be unit-tested.
88
+ *
89
+ * This module previously exported nothing, so running the whole scan at module
90
+ * scope and calling process.exit() was harmless. Adding an export above makes
91
+ * that live: importing it to test the fold would run the repo scan and kill the
92
+ * test process the moment the repo had a real violation. Hence the
93
+ * direct-invocation guard at the bottom.
94
+ */
95
+ // `srcDir`/`rootDir` default to this repo, so the shipped CLI behaviour is
96
+ // unchanged. They exist so the scanner can be driven over a throwaway tree in a
97
+ // test WITHOUT planting a probe file inside `src/` — planting one there both
98
+ // trips SourceTreeGuard (which refuses any delete inside the instar source
99
+ // tree) and is visible to every other test running at the same time.
100
+ export function scanForCredentialedUrlLogs(srcDir = SRC, rootDir = ROOT) {
48
101
  const offenders = [];
49
- for (const file of walk(SRC)) {
50
- const rel = path.relative(ROOT, file);
102
+ for (const file of walk(srcDir)) {
103
+ const rel = path.relative(rootDir, file);
51
104
  if (EXEMPT.includes(rel)) continue;
52
105
  const lines = fs.readFileSync(file, 'utf-8').split('\n');
53
106
  lines.forEach((line, i) => {
54
107
  const hasRedact = line.includes('redactUrl') || line.includes('redactUrlsInText');
55
- if (CREDENTIALED_URL_LITERAL.test(line)) {
108
+ // The FACT half is tested against the folded line; the SPELLING half is
109
+ // tested against the raw line exactly as before (unchanged behaviour).
110
+ if (CREDENTIALED_URL_LITERAL.test(collapseConcatenation(line))) {
56
111
  offenders.push(`${rel}:${i + 1} credentialed-URL literal: ${line.trim().slice(0, 100)}`);
57
112
  } else if (RISKY_URL_VAR_LOG.test(line) && !hasRedact) {
58
113
  offenders.push(`${rel}:${i + 1} logs a clone/remote URL var without redactUrl(): ${line.trim().slice(0, 100)}`);
@@ -60,11 +115,18 @@ for (const file of walk(SRC)) {
60
115
  });
61
116
  }
62
117
 
63
- if (offenders.length > 0) {
64
- console.error('[lint-no-direct-url-log] credentialed-URL logging detected:');
65
- for (const o of offenders) console.error(` - ${o}`);
66
- console.error('\nRoute the URL through redactUrl()/redactUrlsInText() from src/core/redactUrl.ts before logging.');
67
- process.exit(1);
118
+ return offenders;
119
+ }
120
+
121
+ // Only scan + exit when RUN, never when imported (see the note above).
122
+ if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
123
+ const offenders = scanForCredentialedUrlLogs();
124
+ if (offenders.length > 0) {
125
+ console.error('[lint-no-direct-url-log] credentialed-URL logging detected:');
126
+ for (const o of offenders) console.error(` - ${o}`);
127
+ console.error('\nRoute the URL through redactUrl()/redactUrlsInText() from src/core/redactUrl.ts before logging.');
128
+ process.exit(1);
129
+ }
130
+ console.log('[lint-no-direct-url-log] ✓ no credentialed-URL logging found');
131
+ process.exit(0);
68
132
  }
69
- console.log('[lint-no-direct-url-log] ✓ no credentialed-URL logging found');
70
- process.exit(0);
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-08-15T03:30:44.745Z",
5
- "instarVersion": "1.3.1157",
4
+ "generatedAt": "2026-08-15T04:46:02.884Z",
5
+ "instarVersion": "1.3.1159",
6
6
  "entryCount": 202,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -2,7 +2,7 @@
2
2
  "schemaVersion": 1,
3
3
  "generatedFrom": "source-tree",
4
4
  "registrySha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
5
- "packageVersion": "1.3.1157",
5
+ "packageVersion": "1.3.1159",
6
6
  "guards": [
7
7
  {
8
8
  "ref": "docs/audits/phase-b/f10-triage.md",
@@ -1,5 +1,5 @@
1
1
  {
2
- "sha256": "94029728ae76297bbe85baa1e37fd5985de80887bb4432afa090612a0a6da69c",
2
+ "sha256": "64b362d2e7aed4d92d9e9948a954ae1c099930efc5972c89842953e81cfd36ff",
3
3
  "registrySha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
4
- "packageVersion": "1.3.1157"
4
+ "packageVersion": "1.3.1159"
5
5
  }
@@ -2,5 +2,5 @@
2
2
  "sha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
3
3
  "articleCount": 88,
4
4
  "generatedFrom": "docs/STANDARDS-REGISTRY.md",
5
- "packageVersion": "1.3.1157"
5
+ "packageVersion": "1.3.1159"
6
6
  }
@@ -0,0 +1,30 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ `scripts/lint-no-direct-url-log.js` now folds adjacent string-literal
9
+ concatenations before testing its credentialed-URL pattern, so a
10
+ `scheme://user:pass@host` literal split across a `+` is detected as the string it
11
+ actually builds. The sibling variable-name pattern is unchanged.
12
+
13
+ The module also gains a direct-invocation guard: it now exports its scan, and
14
+ without the guard importing it would run the whole repo scan and `process.exit`.
15
+
16
+ ## What to Tell Your User
17
+
18
+ None — internal change (no user-facing surface).
19
+
20
+ ## Summary of New Capabilities
21
+
22
+ None — internal change (no user-facing surface).
23
+
24
+ ## Evidence
25
+
26
+ - 3 new defect cases fail against the shipped fold behaviour; 13 controls pass
27
+ both ways. Source restored byte-identical after the mutation.
28
+ - Four anti-over-block fixtures return identical verdicts under the shipped and
29
+ fixed lint — zero new false positives, measured rather than argued.
30
+ - Real tree exit 0 before and after.
@@ -0,0 +1,40 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ The two vitest globalSetup files that prepare the packed standards-registry asset
9
+ (`tests/setup/build-dist.globalSetup.ts` and
10
+ `tests/setup/ensure-registry-asset.globalSetup.ts`) decided whether to regenerate it
11
+ on PRESENCE alone — `if (outputs.every(exists)) return`. An asset generated from an
12
+ older revision of `docs/STANDARDS-REGISTRY.md` was therefore never regenerated.
13
+
14
+ Both now consult a shared `registryAssetIsStale()` helper that compares the oldest
15
+ output's mtime against the newest input's (`docs/STANDARDS-REGISTRY.md`,
16
+ `docs/standards-registry-floor.json`, `package.json`) — the same shape the sibling
17
+ `ensureDistBuilt()` twelve lines above already used.
18
+
19
+ ## What to Tell Your User
20
+
21
+ None — internal change (no user-facing surface).
22
+
23
+ ## Summary of New Capabilities
24
+
25
+ None — internal change (no user-facing surface).
26
+
27
+ ## Evidence
28
+
29
+ - Measured 2026-08-15: in a checkout whose asset was generated at 17:59 from a source
30
+ three hours newer, `standards-registry-asset`, `standards-enforcement-auditor` and
31
+ `standards-coverage-route` failed with 8 assertions. Regenerating the asset alone
32
+ made all 77 pass.
33
+ - `tests/unit/registry-asset-freshness.test.ts` — 9/9.
34
+ - Negative controls, both fired: reverting the comparison fails THE DEFECT case
35
+ (1 failed / 8 passed); reverting one call site to the presence check fails the
36
+ wiring case, naming the file. Both sources restored byte-exact.
37
+ - The wiring guard matches the MODULE SPECIFIER, not the imported identifier, so an
38
+ aliased import cannot defeat it — the alias blindness found in an invariant test
39
+ earlier this week.
40
+ - `tsc --noEmit` exit 0; full lint chain exit 0 across 45 lints.
@@ -0,0 +1,76 @@
1
+ # Side-effects review — registry asset freshness
2
+
3
+ ## The change
4
+
5
+ Two vitest globalSetup files decided whether to regenerate the packed
6
+ standards-registry asset by asking only whether its outputs EXIST. Both now share a
7
+ `registryAssetIsStale()` helper that compares the oldest output's mtime against the
8
+ newest input's. Test-only; no runtime surface.
9
+
10
+ ## Review answers
11
+
12
+ 1. **Over-block.** This is the dominant risk and it got the most care, because the
13
+ check runs at the START of every suite and an over-eager rule regenerates on every
14
+ run. Three conservative cases, each with its own test: equal mtimes are NOT stale;
15
+ no readable input is NOT stale; an empty output list is NOT stale. Measured cost of
16
+ a genuine regeneration is one generator invocation (~1s), and it self-limits — a
17
+ freshly written asset is newer than its inputs, so the next run skips.
18
+
19
+ 2. **Under-block.** mtime is a proxy for content, not content itself. A source edited
20
+ and reverted leaves a newer mtime and triggers one unnecessary regeneration
21
+ (harmless). A content-identical touch does the same. Conversely, a filesystem that
22
+ does not preserve mtimes, or a checkout that sets all mtimes equal, would report
23
+ fresh when stale. A content hash would be exact — the generator already writes a
24
+ `sha256` into its metadata — and that is the stronger version of this fix. NOT done
25
+ here: it means reading and hashing a 450KB document on every suite start, where the
26
+ mtime compare is four `stat` calls, and the sibling `ensureDistBuilt()` this mirrors
27
+ uses mtimes too. Stated rather than implied.
28
+
29
+ 3. **Level-of-abstraction fit.** Correct layer: the setup layer is where every test
30
+ file inherits the guarantee. The file's own history says this explicitly — a
31
+ per-file `beforeAll` bootstrap was found to be invisible to the next file that
32
+ needed it.
33
+
34
+ 4. **Signal vs authority.** Not a gate. It decides whether to run a generator; it
35
+ blocks nothing and rejects no input.
36
+
37
+ 5. **Interactions.** `ensureRegistryAsset()` must still run AFTER `ensureDistBuilt()`
38
+ (the generator imports from `dist/`). Unchanged — only the early-return predicate
39
+ moved. The exported `ensureRegistryAsset(root)` keeps its signature, so the
40
+ production-generator parity ratchet that imports it is unaffected.
41
+
42
+ 6. **External surfaces.** None. Test setup only; nothing ships to an agent or a user.
43
+
44
+ 7. **Multi-machine posture.** Machine-local BY DESIGN — a per-checkout build artifact.
45
+ There is nothing to replicate: each checkout generates its own copy, and the whole
46
+ defect was one checkout trusting its own stale copy.
47
+
48
+ 8. **Rollback cost.** Revert the commit. The helper is additive; the two call sites
49
+ return to a presence check.
50
+
51
+ ## Class closure — what this does NOT close
52
+
53
+ - **Only these two sites.** A sweep of `tests/setup/` and `scripts/` for
54
+ regenerate-on-presence guards found exactly these two making a regeneration decision
55
+ on existence alone; the other `existsSync` hits are ordinary existence guards, not
56
+ regeneration decisions. The wiring test pins both and fails if either drifts, but it
57
+ knows only about setups that generate THIS asset.
58
+ - **`src/data/builtin-manifest.json`** is named in the generator's own comment as
59
+ having a related defect (generated only into `src/data/` while its reader resolves
60
+ module-relative from `dist/data/`). Different feature, different defect, untouched.
61
+ - **mtime is not content** — see review answer 2.
62
+
63
+ ## Evidence
64
+
65
+ - `tests/unit/registry-asset-freshness.test.ts` — 9/9 green.
66
+ - Negative controls, BOTH fired and both restored byte-exact: reverting the comparison
67
+ fails THE DEFECT case (1 failed / 8 passed — the 8 passing are the controls, which is
68
+ what makes them controls); reverting one call site to the presence check fails the
69
+ wiring case and names the file.
70
+ - Observed live: running the new test in a fresh checkout regenerated the asset
71
+ ("88 articles → src/data + dist/data"). Under the presence check it would have
72
+ skipped, because the outputs existed.
73
+ - The originating measurement: three test files / eight assertions failing against a
74
+ three-hour-stale asset; regeneration alone made all 77 pass.
75
+ - `tsc --noEmit` exit 0 (run via the real binary — `npx tsc` here is intercepted by a
76
+ shim that exits 0 without typechecking). Full lint chain exit 0 across 45 lints.
@@ -0,0 +1,131 @@
1
+ # Side effects — url-log lint resolves split credentialed literals
2
+
3
+ ## 1. Over-block
4
+
5
+ The dominant risk: this lint fails builds, so flagging correct code is more
6
+ expensive than missing a case (a noisy check gets switched off, and then it
7
+ guards nothing).
8
+
9
+ Bounded four ways:
10
+ - the fold joins only ADJACENT string literals of the SAME quote style;
11
+ - a variable operand ENDS the fold, so no runtime value is ever assumed;
12
+ - the fold is bounded to 8 passes and returns the input unchanged if it cannot
13
+ make progress;
14
+ - it can only ever JOIN existing literal text — a test pins that the folded line
15
+ is never longer than the input, so the fold cannot synthesise content.
16
+
17
+ Measured, not asserted: four anti-over-block fixtures were run against BOTH the
18
+ shipped lint and the fixed lint and returned identical verdicts. The real tree
19
+ lints clean before and after.
20
+
21
+ Note one verdict that looks like over-block and is not: a hardcoded
22
+ `https://user:tok@host` literal is flagged even when the log call redacts it.
23
+ That is pre-existing behaviour (the literal branch never consulted the redaction
24
+ check) and it is correct — a credential hardcoded in source is a leak regardless
25
+ of what happens at log time. Verified identical under the shipped lint.
26
+
27
+ ## 2. Under-block — what this does NOT close
28
+
29
+ Stated by kind, because the two halves of this lint fail differently:
30
+
31
+ - **The variable-name half is untouched and still defeatable.** `RISKY_URL_VAR_LOG`
32
+ matches five names through `console.*`. A renamed variable (`originUrl`) and a
33
+ different sink (`logger.info`) both evade it — measured. This is deliberate:
34
+ that pattern matches a SPELLING correlated with the behaviour rather than the
35
+ behaviour, so widening the list makes a finer net and no more of a policy. The
36
+ correct repair is to demote it from decider to candidate-gatherer with the
37
+ weighing downstream, which changes the check's authority and belongs in a spec.
38
+ - Cross-line construction (a credentialed URL assembled over several statements)
39
+ is not resolved — that needs dataflow, not a line-scoped fold.
40
+ - A credentialed URL arriving from config, argv, or another module is invisible
41
+ to any source-text check.
42
+
43
+ ## 3. Level-of-abstraction fit
44
+
45
+ Correct layer. A `user:pass@` inside a URL literal is an exact lexical fact about
46
+ our own source, which is what a deterministic source lint is for. The runtime
47
+ redaction funnel (`src/core/redactUrl.ts`) remains the authority for URLs whose
48
+ credentials only exist at runtime; this lint does not and cannot replace it.
49
+
50
+ ## 4. Signal vs authority
51
+
52
+ Unchanged. The lint holds the same blocking authority it already had, over a
53
+ strictly more accurate view of the same prohibited fact. No new authority, no
54
+ new decision class, no runtime surface.
55
+
56
+ ## 5. Interactions
57
+
58
+ None. No other check reads this one's output. The direct-invocation guard is
59
+ additive — it only changes behaviour on `import`, which nothing did before,
60
+ because the module exported nothing until now.
61
+
62
+ ## 6. External surfaces
63
+
64
+ None. CI-only. No runtime code path, no API, no user-visible behaviour.
65
+
66
+ ## 7. Multi-machine posture
67
+
68
+ Not applicable — machine-local by design. This is a build-time check over source
69
+ text in a checkout; it holds no state and replicates nothing.
70
+
71
+ ## 8. Rollback cost
72
+
73
+ Revert the commit. The lint returns to its previous matching behaviour; nothing
74
+ persists and no state migrates.
75
+
76
+ ---
77
+
78
+ ## Addendum — CI caught a guard conflict in the TEST, and the fix is a redesign not a workaround
79
+
80
+ **What CI found.** `Unit Tests shard 1/4` failed on node 20 AND node 22 (same shard both versions, so
81
+ not a flake): `SourceTreeGuardError: Refusing to run ... (requested dir:
82
+ .../src/core/__urlLogLintProbe.ts, resolved git root: ...)`. The test planted a probe file inside
83
+ `src/` so the real lint would scan it, then removed it through `SafeFsExecutor` — and SourceTreeGuard
84
+ refuses ANY delete inside the instar source tree (the 2026-04-22 incident class).
85
+
86
+ **Why it passed locally and failed in CI is the honest part:** I did not establish that, and I am not
87
+ guessing at it. What I did establish is that the test design was wrong on its own terms regardless of
88
+ which environment surfaces it.
89
+
90
+ **Two defects in that design, and the second is the one I had not considered:**
91
+ 1. It performs a destructive operation inside the source tree — exactly what the guard exists to stop.
92
+ Routing it through the audited funnel satisfied the destructive-op lint and walked into a different
93
+ guard. Satisfying one guard is not evidence about another.
94
+ 2. A probe file sitting at `src/core/__urlLogLintProbe.ts` is **visible to every other suite running at
95
+ the same time**. That is shared mutable state in a shared tree, and it would have been a latent
96
+ flake source for other people's tests, not just mine.
97
+
98
+ **The fix.** `scanForCredentialedUrlLogs()` now takes optional `srcDir`/`rootDir` that DEFAULT to this
99
+ repo, so the shipped CLI behaviour is unchanged. The test scans a throwaway temp tree instead. Nothing
100
+ is written into `src/` and nothing is deleted inside the source tree.
101
+
102
+ ### Review answers for this addendum
103
+
104
+ 1. **Over-block.** None new. The CLI path takes the defaults and is behaviourally identical — verified
105
+ by running it before and after the change (`exit 0` both times) and by the real-tree control below.
106
+ 2. **Under-block.** One thing genuinely got WEAKER and I am naming it rather than letting it pass: the
107
+ defect cases now drive the exported scanner, so they no longer exercise the CLI's
108
+ offender-printing + `process.exit(1)` path end to end. A new test keeps the exit-code path covered
109
+ in the passing direction (`the shipped CLI still exits 0 on the clean tree`), but the failing
110
+ direction of the CLI wrapper is uncovered. That wrapper is eight lines and unchanged by this PR.
111
+ 3. **New surface introduced.** The scanner now trusts a caller-supplied root. A caller could point it
112
+ at an empty directory and receive a clean verdict — but this is a lint's test seam, not a decision
113
+ authority, and the CLI never passes arguments. Named because "a scan over nothing reports clean" is
114
+ the exact defect I fixed in `lint-no-direct-destructive` earlier this window; here it is reachable
115
+ only by a caller that deliberately supplies the wrong root.
116
+ 4. **Signal vs authority.** Unchanged — still a deterministic detector with a build-failing exit code.
117
+ 5. **Interactions.** Removes an interaction rather than adding one: no more shared probe file inside
118
+ `src/` for concurrent suites to observe.
119
+ 6. **Multi-machine posture.** Machine-local by design — a lint script and its unit test.
120
+ 7. **Rollback cost.** Revert the commit; the parameters are additive with defaults.
121
+
122
+ ### Evidence
123
+
124
+ - `tests/unit/url-log-lint-split-literal.test.ts` — **17/17 green** (16 before; +1 for the CLI exit path).
125
+ - **Negative control re-run after the redesign, because rewriting HOW a test drives its subject can
126
+ quietly turn it into a check that cannot fail:** removing the fold from the decision makes exactly
127
+ the **3 defect cases fail**, 14 pass both ways. Source restored byte-exact (sha match, 0 markers).
128
+ - Real-tree anti-over-block control is now STRONGER: it asserts `scanForCredentialedUrlLogs()` returns
129
+ `[]` under the real defaults, instead of asserting a temp tree with one inert line is clean.
130
+ - `tsc --noEmit` exit 0 (run via the real binary — `npx tsc` here is a shim that exits 0 without
131
+ typechecking). Full lint chain exit 0.