instar 1.3.1157 → 1.3.1158

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.1158",
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": "9cbf312433ed10eafb3f33a245b9ba17546fdd534ce1c2caf3dcaf28157ee33c",
3
3
  "registrySha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
4
- "packageVersion": "1.3.1157"
4
+ "packageVersion": "1.3.1158"
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.1158"
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.1158",
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:14:15.394Z",
5
+ "instarVersion": "1.3.1158",
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.1158",
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": "9cbf312433ed10eafb3f33a245b9ba17546fdd534ce1c2caf3dcaf28157ee33c",
3
3
  "registrySha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
4
- "packageVersion": "1.3.1157"
4
+ "packageVersion": "1.3.1158"
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.1158"
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,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.