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.
- package/dist/data/standards-guard-index.json +1 -1
- package/dist/data/standards-guard-index.meta.json +2 -2
- package/dist/data/standards-registry.meta.json +1 -1
- package/package.json +1 -1
- package/scripts/lint-no-direct-url-log.js +72 -10
- package/src/data/builtin-manifest.json +2 -2
- package/src/data/standards-guard-index.json +1 -1
- package/src/data/standards-guard-index.meta.json +2 -2
- package/src/data/standards-registry.meta.json +1 -1
- package/upgrades/1.3.1158.md +30 -0
- package/upgrades/1.3.1159.md +40 -0
- package/upgrades/side-effects/registry-asset-freshness.md +76 -0
- package/upgrades/side-effects/url-log-lint-split-literal.md +131 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"generatedFrom": "source-tree",
|
|
4
4
|
"registrySha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
|
|
5
|
-
"packageVersion": "1.3.
|
|
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": "
|
|
2
|
+
"sha256": "64b362d2e7aed4d92d9e9948a954ae1c099930efc5972c89842953e81cfd36ff",
|
|
3
3
|
"registrySha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
|
|
4
|
-
"packageVersion": "1.3.
|
|
4
|
+
"packageVersion": "1.3.1159"
|
|
5
5
|
}
|
package/package.json
CHANGED
|
@@ -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(
|
|
50
|
-
const rel = path.relative(
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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-
|
|
5
|
-
"instarVersion": "1.3.
|
|
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.
|
|
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": "
|
|
2
|
+
"sha256": "64b362d2e7aed4d92d9e9948a954ae1c099930efc5972c89842953e81cfd36ff",
|
|
3
3
|
"registrySha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
|
|
4
|
-
"packageVersion": "1.3.
|
|
4
|
+
"packageVersion": "1.3.1159"
|
|
5
5
|
}
|
|
@@ -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.
|