@@ -38,7 +38,7 @@ const ANNOTATION_RE =
3838const ALLOW_MARKER = '# socket-lint: allow soak-exclude-no-date-annotation'
3939
4040export interface Finding {
41- kind: 'missing' | 'stale'
41+ kind: 'missing' | 'stale' | 'unpinned'
4242 line: number
4343 name: string
4444 version: string
@@ -62,14 +62,30 @@ export function scan(text: string, todayISO: string): Finding[] {
6262 inBlock = false
6363 continue
6464 }
65- const m = ENTRY_RE . exec ( line )
66- if ( ! m ) {
65+ if ( line . includes ( ALLOW_MARKER ) ) {
6766 continue
6867 }
69- if ( line . includes ( ALLOW_MARKER ) ) {
68+ // Glob entries (`@scope/*`, `socket-*`) trust a whole first-party scope —
69+ // exempt from version-pinning by design.
70+ if ( GLOB_ENTRY_RE . test ( line ) ) {
7071 continue
7172 }
72- if ( GLOB_ENTRY_RE . test ( line ) || BARE_NAME_ENTRY_RE . test ( line ) ) {
73+ // A concrete (non-glob) entry MUST be version-pinned: `name@version`. A bare
74+ // name pins no version, so the soak-bypass leaks to every future release of
75+ // the package — exactly the gap a dated `# published:/removable:` annotation
76+ // is supposed to scope. Flag it.
77+ if ( BARE_NAME_ENTRY_RE . test ( line ) ) {
78+ const bareName = / ^ \s * - \s * [ ' " ] ? ( [ ^ ' " \s ] + ) [ ' " ] ? \s * $ / . exec ( line ) ?. [ 1 ] ?? '<unknown>'
79+ findings . push ( {
80+ kind : 'unpinned' ,
81+ line : i + 1 ,
82+ name : bareName ,
83+ version : '<none>' ,
84+ } )
85+ continue
86+ }
87+ const m = ENTRY_RE . exec ( line )
88+ if ( ! m ) {
7389 continue
7490 }
7591 const name = m [ 1 ] ?? '<unknown>'
@@ -136,6 +152,7 @@ function main(): void {
136152 const findings = scan ( content , todayISO )
137153 const missing = findings . filter ( f => f . kind === 'missing' )
138154 const stale = findings . filter ( f => f . kind === 'stale' )
155+ const unpinned = findings . filter ( f => f . kind === 'unpinned' )
139156
140157 if ( stale . length > 0 && fix ) {
141158 // Promote: the soak cleared, so the bypass is no longer needed.
@@ -188,6 +205,26 @@ function main(): void {
188205 process . exit ( 1 )
189206 }
190207
208+ if ( unpinned . length > 0 ) {
209+ process . stderr . write (
210+ `[check-soak-excludes-have-dates] ${ unpinned . length } unpinned third-party ` +
211+ `soak-exclude entr${ unpinned . length === 1 ? 'y' : 'ies' } (bare name, no ` +
212+ `\`@version\`):\n` ,
213+ )
214+ for ( let i = 0 , { length } = unpinned ; i < length ; i += 1 ) {
215+ const f = unpinned [ i ] !
216+ process . stderr . write ( ` line ${ f . line } : ${ f . name } \n` )
217+ }
218+ process . stderr . write (
219+ `\nA concrete soak-exclude must pin the exact version, so the bypass can't ` +
220+ `leak to a future release:\n` +
221+ ` - 'pkg@1.2.3' not - 'pkg'\n` +
222+ `First-party scope globs (\`@scope/*\`, \`socket-*\`) are exempt.\n` +
223+ `Reference: docs/agents.md/fleet/tooling.md "Soak time".\n` ,
224+ )
225+ process . exit ( 1 )
226+ }
227+
191228 process . exit ( 0 )
192229}
193230
0 commit comments