dsh-permission-rules 0.3.0 → 0.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.
package/src/config.ts CHANGED
@@ -64,6 +64,17 @@ export interface Config {
64
64
  * policy in production before enforcing it.
65
65
  */
66
66
  enforce?: boolean
67
+ /**
68
+ * Whether to keep appending audit events to the session log on hosts
69
+ * whose `Session.append` predates the `ignorable` envelope marker
70
+ * (the `0.1.0-rc.6` line). Defaults to `false`: such hosts write the
71
+ * events UNMARKED, which makes the session unresumable on stricter
72
+ * hosts, so the runtime detects the host and disables session-log audit
73
+ * with a one-time warning instead. Set `true` to opt back into the
74
+ * in-session audit trail (and accept that those sessions may need
75
+ * `scripts/repair-session-logs.mjs` before loading on a newer harness).
76
+ */
77
+ allowUnmarkedAudit?: boolean
67
78
  }
68
79
 
69
80
  /** Config after {@link resolveConfig}: every optional field has its explicit default. */
@@ -82,6 +93,7 @@ export interface ResolvedConfig {
82
93
  readonly searchUp: boolean
83
94
  readonly maxGlobStars: number
84
95
  readonly enforce: boolean
96
+ readonly allowUnmarkedAudit: boolean
85
97
  }
86
98
 
87
99
  /** Schemastery schema: the loader validates and fills defaults before `apply`. */
@@ -100,6 +112,7 @@ export const Config: z<Config> = z.object({
100
112
  searchUp: z.boolean().default(false),
101
113
  maxGlobStars: z.number().default(2),
102
114
  enforce: z.boolean().default(true),
115
+ allowUnmarkedAudit: z.boolean().default(false),
103
116
  })
104
117
 
105
118
  /**
@@ -142,6 +155,7 @@ export function resolveConfig(config: Config = {}): ResolvedConfig {
142
155
  assertBoolean('searchUp', searchUp)
143
156
  assertBoolean('caseInsensitivePaths', config.caseInsensitivePaths ?? process.platform === 'win32')
144
157
  assertBoolean('enforce', config.enforce ?? true)
158
+ assertBoolean('allowUnmarkedAudit', config.allowUnmarkedAudit ?? false)
145
159
  return {
146
160
  rulesFile,
147
161
  fallbackPath: config.fallbackPath,
@@ -157,6 +171,7 @@ export function resolveConfig(config: Config = {}): ResolvedConfig {
157
171
  searchUp,
158
172
  maxGlobStars,
159
173
  enforce: config.enforce ?? true,
174
+ allowUnmarkedAudit: config.allowUnmarkedAudit ?? false,
160
175
  }
161
176
  }
162
177
 
package/src/events.ts CHANGED
@@ -17,9 +17,16 @@
17
17
  * downstream — an auditable "what would the policy have done" trail.
18
18
  *
19
19
  * The event is appended with the envelope's `ignorable: true` marker (see
20
- * {@link AuditAppend}), so a harness build whose generated event vocabulary
21
- * does not include this out-of-repo type still loads the log — it skips the
22
- * audit record instead of refusing the whole session.
20
+ * {@link AuditAppend}). Harness builds that honor the marker (post-rc.6)
21
+ * stamp it on the envelope and skip unknown ignorable records when loading,
22
+ * so the audit can never refuse a session. Builds whose `Session.append`
23
+ * predates the marker — the `0.1.0-rc.6` line — silently DROP the options
24
+ * bag: the event then lands UNMARKED and makes the session unresumable on
25
+ * hosts with required-on-read semantics (`SessionFormatUnsupportedError`).
26
+ * The runtime detects this at first use (peer version pre-check plus a
27
+ * probe of the appended envelope) and disables session-log audit on such
28
+ * hosts with a one-time warning; `allowUnmarkedAudit: true` opts back in,
29
+ * and `scripts/repair-session-logs.mjs` repairs already-polluted logs.
23
30
  * @module dsh-permission-rules/events
24
31
  */
25
32
 
@@ -77,12 +84,27 @@ export interface AuditDecision {
77
84
  /**
78
85
  * `Session.append` narrowed to this plugin's audit event. The options bag
79
86
  * exists only on host builds that expose the `ignorable` envelope-marker
80
- * surface (post-rc.6 `@deepseek-ai/dsh-session`); an rc.6 host accepts and
81
- * ignores the third argument, appending the identical event without the
82
- * marker — no behavior change, no failure either way.
87
+ * surface (post-rc.6 `@deepseek-ai/dsh-session`); an rc.6 host accepts the
88
+ * call but silently drops the third argument — the event is appended
89
+ * WITHOUT the marker, which is exactly what breaks later resume on stricter
90
+ * hosts. The runtime treats the marker as optional-but-probed: see
91
+ * {@link isMarkedAuditEvent}.
83
92
  */
84
93
  export type AuditAppend = (
85
94
  type: 'permissionRules/decision',
86
95
  data: AuditDecision,
87
96
  options?: { ignorable?: true },
88
97
  ) => unknown
98
+
99
+ /**
100
+ * Whether an `append` call actually honored the `ignorable` marker: the
101
+ * logged event returned by the host carries `ignorable === true` on
102
+ * marker-aware builds and nothing on pre-marker builds. `false` (or any
103
+ * non-event return) means the host dropped the marker and the event landed
104
+ * unmarked — the runtime then degrades instead of polluting further logs.
105
+ * @param result - the return value of the audit append.
106
+ * @returns true only when the marker is present on the returned envelope.
107
+ */
108
+ export function isMarkedAuditEvent(result: unknown): boolean {
109
+ return typeof result === 'object' && result !== null && (result as { ignorable?: unknown }).ignorable === true
110
+ }
package/src/index.ts CHANGED
@@ -22,7 +22,7 @@ export const name = 'permission-rules'
22
22
  export const inject = ['commands', 'tools']
23
23
 
24
24
  export { apply }
25
- export { PermissionRulesRuntime } from './runtime.ts'
25
+ export { PermissionRulesRuntime, isUnmarkedHostVersion } from './runtime.ts'
26
26
  export * from './config.ts'
27
27
  export * from './events.ts'
28
28
  export * from './rules.ts'
package/src/prose.ts CHANGED
@@ -34,6 +34,8 @@ export interface UiProse {
34
34
  decisionsHeader: (shown: number, total: number) => string
35
35
  /** Line shown when the session log holds no audit decisions. */
36
36
  noDecisions: string
37
+ /** Notice appended to `/rules decisions` when session-log audit is disabled on this host (ignorable marker not honored). */
38
+ auditDisabledNotice: string
37
39
  /** Rejection of a malformed decisions count. */
38
40
  invalidDecisionsCount: (arg: string) => string
39
41
  /** One audit-decision row (dry-run rows mark the actual outcome). */
@@ -48,6 +50,8 @@ export interface UiProse {
48
50
  testUnknownFlag: (flag: string) => string
49
51
  /** Rejection of a malformed `/rules test` flag (missing value, bad KEY=VALUE). */
50
52
  testBadFlag: (flag: string) => string
53
+ /** Rejection of an unknown `--platform` value. */
54
+ testBadPlatform: (value: string) => string
51
55
  /** Usage hint of `/rules test`. */
52
56
  testUsage: string
53
57
  /** Warning naming shadowed (unreachable) rules by 1-based number. */
@@ -68,6 +72,8 @@ export interface DescribeTokens {
68
72
  platform: string
69
73
  disabled: string
70
74
  tags: string
75
+ /** Prefix of the per-rule source-file attribution. */
76
+ src: string
71
77
  }
72
78
 
73
79
  /** Truncate long reasons/descriptions in one-line displays. */
@@ -84,9 +90,10 @@ const EN: UiProse = {
84
90
  lastReloadWarning: error => `Warning: the last reload failed (${error}); the rules listed above are the previous ones.`,
85
91
  dryRunNotice: 'Dry-run mode (enforce: false): deny/ask hits are audit-logged only — every call is passed through.',
86
92
  unknownArg: arg => `Unknown /rules argument "${arg}". ${EN.usage}`,
87
- usage: 'Usage: /rules [reload | decisions [n] | test <tool> <json-args>]',
93
+ usage: 'Usage: /rules [list | reload | decisions [n] | test <tool> <json-args>]',
88
94
  decisionsHeader: (shown, total) => `Last ${shown} of ${total} permission decision(s) for this session:`,
89
95
  noDecisions: 'No permission decisions recorded in this session yet.',
96
+ auditDisabledNotice: 'Session-log audit is disabled on this host: its Session.append predates the ignorable marker and would make sessions unresumable elsewhere (set allowUnmarkedAudit: true to opt back in).',
90
97
  invalidDecisionsCount: arg => `Invalid decisions count "${arg}": give a positive integer (default 10).`,
91
98
  decisionLine: (seq, action, toolName, ruleIndex, reason, dryRun, outcome) => {
92
99
  const rule = ruleIndex === undefined ? '' : ` (rule ${ruleIndex + 1})`
@@ -99,7 +106,8 @@ const EN: UiProse = {
99
106
  testBadJson: text => `Invalid JSON arguments "${text}": the arguments must parse as JSON.`,
100
107
  testUnknownFlag: flag => `Unknown /rules test flag "${flag}". ${EN.testUsage}`,
101
108
  testBadFlag: flag => `Invalid /rules test flag "${flag}": expected a value. ${EN.testUsage}`,
102
- testUsage: 'Usage: /rules test [--cwd <dir>] [--env KEY=VALUE]... [--agent <selector>]... <tool> <json-args>, e.g. /rules test bash {"command":"git push origin main"}',
109
+ testBadPlatform: value => `Unknown platform "${value}" in --platform: expected one of aix, android, darwin, freebsd, linux, openbsd, sunos, win32. ${EN.testUsage}`,
110
+ testUsage: 'Usage: /rules test [--cwd <dir>] [--env KEY=VALUE]... [--agent <selector>]... [--platform <name>] <tool> <json-args>, e.g. /rules test bash {"command":"git push origin main"}',
103
111
  unreachableWarning: numbers => `Warning: rule${numbers.length > 1 ? 's' : ''} ${numbers.join(', ')} ${numbers.length > 1 ? 'are' : 'is'} unreachable (shadowed by an earlier catch-all rule).`,
104
112
  emptySource: '(no rule file — empty rule set)',
105
113
  }
@@ -113,9 +121,10 @@ const ZH: UiProse = {
113
121
  lastReloadWarning: error => `警告:上次重载失败(${error});以上列出的是之前的规则。`,
114
122
  dryRunNotice: '干跑模式(enforce: false):deny/ask 命中仅写入审计日志——所有调用照常透传。',
115
123
  unknownArg: arg => `未知的 /rules 参数 "${arg}"。${ZH.usage}`,
116
- usage: '用法:/rules [reload | decisions [n] | test <工具名> <json-参数>]',
124
+ usage: '用法:/rules [list | reload | decisions [n] | test <工具名> <json-参数>]',
117
125
  decisionsHeader: (shown, total) => `本会话最近 ${shown}/${total} 条权限裁决:`,
118
126
  noDecisions: '本会话还没有记录任何权限裁决。',
127
+ auditDisabledNotice: '本宿主上会话日志审计已停用:其 Session.append 早于 ignorable 标记支持,会令会话在其他宿主上无法恢复(设 allowUnmarkedAudit: true 可重新开启)。',
119
128
  invalidDecisionsCount: arg => `无效的裁决条数 "${arg}":请给正整数(默认 10)。`,
120
129
  decisionLine: (seq, action, toolName, ruleIndex, reason, dryRun, outcome) => {
121
130
  const rule = ruleIndex === undefined ? '' : `(规则 ${ruleIndex + 1})`
@@ -128,7 +137,8 @@ const ZH: UiProse = {
128
137
  testBadJson: text => `无效的 JSON 参数 "${text}":参数必须能被解析为 JSON。`,
129
138
  testUnknownFlag: flag => `未知的 /rules test 标志 "${flag}"。${ZH.testUsage}`,
130
139
  testBadFlag: flag => `无效的 /rules test 标志 "${flag}":缺少取值。${ZH.testUsage}`,
131
- testUsage: '用法:/rules test [--cwd <目录>] [--env 键=值]... [--agent <选择器>]... <工具名> <json-参数>,例如 /rules test bash {"command":"git push origin main"}',
140
+ testBadPlatform: value => `未知的平台 "${value}"(--platform):应为 aix、android、darwin、freebsd、linux、openbsd、sunos、win32 之一。${ZH.testUsage}`,
141
+ testUsage: '用法:/rules test [--cwd <目录>] [--env 键=值]... [--agent <选择器>]... [--platform <平台名>] <工具名> <json-参数>,例如 /rules test bash {"command":"git push origin main"}',
132
142
  unreachableWarning: numbers => `警告:规则 ${numbers.join('、')} 不可达(被前面的通配规则遮蔽)。`,
133
143
  emptySource: '(无规则文件——空规则集)',
134
144
  }
@@ -142,9 +152,10 @@ const ES: UiProse = {
142
152
  lastReloadWarning: error => `Aviso: la última recarga falló (${error}); las reglas mostradas son las anteriores.`,
143
153
  dryRunNotice: 'Modo simulación (enforce: false): los aciertos deny/ask solo se registran en auditoría — todas las llamadas pasan.',
144
154
  unknownArg: arg => `Argumento de /rules desconocido "${arg}". ${ES.usage}`,
145
- usage: 'Uso: /rules [reload | decisions [n] | test <herramienta> <json-args>]',
155
+ usage: 'Uso: /rules [list | reload | decisions [n] | test <herramienta> <json-args>]',
146
156
  decisionsHeader: (shown, total) => `Últimas ${shown} de ${total} decisión(es) de permiso de esta sesión:`,
147
157
  noDecisions: 'Esta sesión aún no registra decisiones de permiso.',
158
+ auditDisabledNotice: 'La auditoría del registro de sesión está desactivada en este host: su Session.append es anterior al marcador ignorable y haría las sesiones irrecuperables en otros hosts (establece allowUnmarkedAudit: true para reactivarla).',
148
159
  invalidDecisionsCount: arg => `Cantidad de decisiones inválida "${arg}": indique un entero positivo (por defecto 10).`,
149
160
  decisionLine: (seq, action, toolName, ruleIndex, reason, dryRun, outcome) => {
150
161
  const rule = ruleIndex === undefined ? '' : ` (regla ${ruleIndex + 1})`
@@ -157,7 +168,8 @@ const ES: UiProse = {
157
168
  testBadJson: text => `Argumentos JSON inválidos "${text}": los argumentos deben poderse analizar como JSON.`,
158
169
  testUnknownFlag: flag => `Bandera de /rules test desconocida "${flag}". ${ES.testUsage}`,
159
170
  testBadFlag: flag => `Bandera de /rules test inválida "${flag}": falta un valor. ${ES.testUsage}`,
160
- testUsage: 'Uso: /rules test [--cwd <dir>] [--env CLAVE=VALOR]... [--agent <selector>]... <herramienta> <json-args>, p. ej. /rules test bash {"command":"git push origin main"}',
171
+ testBadPlatform: value => `Plataforma desconocida "${value}" en --platform: se esperaba una de aix, android, darwin, freebsd, linux, openbsd, sunos, win32. ${ES.testUsage}`,
172
+ testUsage: 'Uso: /rules test [--cwd <dir>] [--env CLAVE=VALOR]... [--agent <selector>]... [--platform <nombre>] <herramienta> <json-args>, p. ej. /rules test bash {"command":"git push origin main"}',
161
173
  unreachableWarning: numbers => `Aviso: ${numbers.length > 1 ? 'las reglas' : 'la regla'} ${numbers.join(', ')} ${numbers.length > 1 ? 'son inalcanzables' : 'es inalcanzable'} (tapada por una regla general anterior).`,
162
174
  emptySource: '(sin archivo de reglas — conjunto vacío)',
163
175
  }
@@ -171,9 +183,10 @@ const PT: UiProse = {
171
183
  lastReloadWarning: error => `Aviso: a última recarga falhou (${error}); as regras listadas são as anteriores.`,
172
184
  dryRunNotice: 'Modo simulação (enforce: false): acertos deny/ask são apenas registrados na auditoria — todas as chamadas passam.',
173
185
  unknownArg: arg => `Argumento de /rules desconhecido "${arg}". ${PT.usage}`,
174
- usage: 'Uso: /rules [reload | decisions [n] | test <ferramenta> <json-args>]',
186
+ usage: 'Uso: /rules [list | reload | decisions [n] | test <ferramenta> <json-args>]',
175
187
  decisionsHeader: (shown, total) => `Últimas ${shown} de ${total} decisão(ões) de permissão desta sessão:`,
176
188
  noDecisions: 'Esta sessão ainda não registrou decisões de permissão.',
189
+ auditDisabledNotice: 'A auditoria do registro da sessão está desativada neste host: seu Session.append é anterior ao marcador ignorable e tornaria as sessões irrecuperáveis em outros hosts (defina allowUnmarkedAudit: true para reativar).',
177
190
  invalidDecisionsCount: arg => `Quantidade de decisões inválida "${arg}": informe um inteiro positivo (padrão 10).`,
178
191
  decisionLine: (seq, action, toolName, ruleIndex, reason, dryRun, outcome) => {
179
192
  const rule = ruleIndex === undefined ? '' : ` (regra ${ruleIndex + 1})`
@@ -186,7 +199,8 @@ const PT: UiProse = {
186
199
  testBadJson: text => `Argumentos JSON inválidos "${text}": os argumentos precisam ser JSON analisável.`,
187
200
  testUnknownFlag: flag => `Sinalizador de /rules test desconhecido "${flag}". ${PT.testUsage}`,
188
201
  testBadFlag: flag => `Sinalizador de /rules test inválido "${flag}": falta um valor. ${PT.testUsage}`,
189
- testUsage: 'Uso: /rules test [--cwd <dir>] [--env CHAVE=VALOR]... [--agent <seletor>]... <ferramenta> <json-args>, ex.: /rules test bash {"command":"git push origin main"}',
202
+ testBadPlatform: value => `Plataforma desconhecida "${value}" em --platform: esperava-se uma de aix, android, darwin, freebsd, linux, openbsd, sunos, win32. ${PT.testUsage}`,
203
+ testUsage: 'Uso: /rules test [--cwd <dir>] [--env CHAVE=VALOR]... [--agent <seletor>]... [--platform <nome>] <ferramenta> <json-args>, ex.: /rules test bash {"command":"git push origin main"}',
190
204
  unreachableWarning: numbers => `Aviso: ${numbers.length > 1 ? 'as regras' : 'a regra'} ${numbers.join(', ')} ${numbers.length > 1 ? 'são inalcançáveis' : 'é inalcançável'} (encoberta por uma regra geral anterior).`,
191
205
  emptySource: '(sem arquivo de regras — conjunto vazio)',
192
206
  }
@@ -200,9 +214,10 @@ const HI: UiProse = {
200
214
  lastReloadWarning: error => `चेतावनी: अंतिम पुनः लोड विफल रहा (${error}); ऊपर सूचीबद्ध नियम पिछले वाले हैं।`,
201
215
  dryRunNotice: 'ड्राई-रन मोड (enforce: false): deny/ask हिट केवल ऑडिट में दर्ज होते हैं — हर कॉल पास हो जाती है।',
202
216
  unknownArg: arg => `अज्ञात /rules तर्क "${arg}"। ${HI.usage}`,
203
- usage: 'उपयोग: /rules [reload | decisions [n] | test <टूल> <json-args>]',
217
+ usage: 'उपयोग: /rules [list | reload | decisions [n] | test <टूल> <json-args>]',
204
218
  decisionsHeader: (shown, total) => `इस सत्र के अंतिम ${shown}/${total} अनुमति निर्णय:`,
205
219
  noDecisions: 'इस सत्र में अभी कोई अनुमति निर्णय दर्ज नहीं है।',
220
+ auditDisabledNotice: 'इस होस्ट पर सत्र-लॉग ऑडिट अक्षम है: इसका Session.append ignorable मार्कर से पुराना है और सत्र अन्य होस्ट पर अप्राप्य बन जाते (पुनः चालू करने के लिए allowUnmarkedAudit: true सेट करें)।',
206
221
  invalidDecisionsCount: arg => `अमान्य निर्णय-संख्या "${arg}": एक धनात्मक पूर्णांक दें (डिफ़ॉल्ट 10)।`,
207
222
  decisionLine: (seq, action, toolName, ruleIndex, reason, dryRun, outcome) => {
208
223
  const rule = ruleIndex === undefined ? '' : ` (नियम ${ruleIndex + 1})`
@@ -215,7 +230,8 @@ const HI: UiProse = {
215
230
  testBadJson: text => `अमान्य JSON तर्क "${text}": तर्क JSON के रूप में पार्स होने चाहिए।`,
216
231
  testUnknownFlag: flag => `अज्ञात /rules test फ़्लैग "${flag}"। ${HI.testUsage}`,
217
232
  testBadFlag: flag => `अमान्य /rules test फ़्लैग "${flag}": मान गायब है। ${HI.testUsage}`,
218
- testUsage: 'उपयोग: /rules test [--cwd <dir>] [--env KEY=मान]... [--agent <चयनकर्ता>]... <टूल> <json-args>, जैसे /rules test bash {"command":"git push origin main"}',
233
+ testBadPlatform: value => `अज्ञात प्लेटफ़ॉर्म "${value}" (--platform): aix, android, darwin, freebsd, linux, openbsd, sunos, win32 में से एक अपेक्षित। ${HI.testUsage}`,
234
+ testUsage: 'उपयोग: /rules test [--cwd <dir>] [--env KEY=मान]... [--agent <चयनकर्ता>]... [--platform <नाम>] <टूल> <json-args>, जैसे /rules test bash {"command":"git push origin main"}',
219
235
  unreachableWarning: numbers => `चेतावनी: नियम ${numbers.join(', ')} अप्राप्य हैं (पहले वाले सर्व-मिलान नियम से ढके हुए)।`,
220
236
  emptySource: '(कोई नियम फ़ाइल नहीं — खाली नियम-समूह)',
221
237
  }
@@ -225,9 +241,9 @@ export const UI_PROSE: Readonly<Record<UiLanguage, UiProse>> = { en: EN, zh: ZH,
225
241
 
226
242
  /** `describeRule` match-dimension tokens by language. */
227
243
  export const DESCRIBE_TOKENS: Readonly<Record<UiLanguage, DescribeTokens>> = {
228
- en: { allTools: 'all tools', tools: 'tools', agents: 'agents', params: 'params', paths: 'paths', absent: 'absent', when: 'when', platform: 'platform', disabled: 'disabled', tags: 'tags' },
229
- zh: { allTools: '全部工具', tools: '工具', agents: '代理', params: '参数', paths: '路径', absent: '缺省键', when: '条件', platform: '平台', disabled: '已禁用', tags: '标签' },
230
- es: { allTools: 'todas las herramientas', tools: 'herramientas', agents: 'agentes', params: 'parámetros', paths: 'rutas', absent: 'ausentes', when: 'cuándo', platform: 'plataforma', disabled: 'deshabilitada', tags: 'etiquetas' },
231
- pt: { allTools: 'todas as ferramentas', tools: 'ferramentas', agents: 'agentes', params: 'parâmetros', paths: 'caminhos', absent: 'ausentes', when: 'quando', platform: 'plataforma', disabled: 'desativada', tags: 'etiquetas' },
232
- hi: { allTools: 'सभी टूल', tools: 'टूल', agents: 'एजेंट', params: 'पैरामीटर', paths: 'पथ', absent: 'अनुपस्थित', when: 'शर्त', platform: 'प्लेटफ़ॉर्म', disabled: 'अक्षम', tags: 'टैग' },
244
+ en: { allTools: 'all tools', tools: 'tools', agents: 'agents', params: 'params', paths: 'paths', absent: 'absent', when: 'when', platform: 'platform', disabled: 'disabled', tags: 'tags', src: 'src' },
245
+ zh: { allTools: '全部工具', tools: '工具', agents: '代理', params: '参数', paths: '路径', absent: '缺省键', when: '条件', platform: '平台', disabled: '已禁用', tags: '标签', src: '来源' },
246
+ es: { allTools: 'todas las herramientas', tools: 'herramientas', agents: 'agentes', params: 'parámetros', paths: 'rutas', absent: 'ausentes', when: 'cuándo', platform: 'plataforma', disabled: 'deshabilitada', tags: 'etiquetas', src: 'origen' },
247
+ pt: { allTools: 'todas as ferramentas', tools: 'ferramentas', agents: 'agentes', params: 'parâmetros', paths: 'caminhos', absent: 'ausentes', when: 'quando', platform: 'plataforma', disabled: 'desativada', tags: 'etiquetas', src: 'origem' },
248
+ hi: { allTools: 'सभी टूल', tools: 'टूल', agents: 'एजेंट', params: 'पैरामीटर', paths: 'पथ', absent: 'अनुपस्थित', when: 'शर्त', platform: 'प्लेटफ़ॉर्म', disabled: 'अक्षम', tags: 'टैग', src: 'स्रोत' },
233
249
  }
package/src/rules.ts CHANGED
@@ -321,12 +321,16 @@ function isCatchAll(rule: CompiledRule): boolean {
321
321
  /**
322
322
  * One-line human summary of a compiled rule, for `/rules` output. The
323
323
  * 1-based number, action, match dimensions, and reason render first; a
324
- * disabled marker, tags, and a truncated description follow.
324
+ * disabled marker, tags, and a truncated description follow. When `source`
325
+ * is provided (multi-file chains), the rule's origin file is attributed on
326
+ * the line.
325
327
  * @param rule - the compiled rule.
326
328
  * @param tokens - dimension-prefix vocabulary for the configured language.
329
+ * @param source - display path of the rule's source file, when the listing
330
+ * spans several files.
327
331
  * @returns a single display line.
328
332
  */
329
- export function describeRule(rule: CompiledRule, tokens: DescribeTokens): string {
333
+ export function describeRule(rule: CompiledRule, tokens: DescribeTokens, source?: string): string {
330
334
  const parts: string[] = []
331
335
  if (rule.source.match.tools.length > 0) parts.push(`${tokens.tools}:${rule.source.match.tools.join(',')}`)
332
336
  if (rule.source.match.agents.length > 0) parts.push(`${tokens.agents}:${rule.source.match.agents.join(',')}`)
@@ -343,9 +347,10 @@ export function describeRule(rule: CompiledRule, tokens: DescribeTokens): string
343
347
  if (whenParts.length > 0 || platformPart.length > 0) parts.push(`${tokens.when}:${whenParts}${whenParts.length > 0 && platformPart.length > 0 ? ' ' : ''}${platformPart}`)
344
348
  const match = parts.length > 0 ? `[${parts.join(' ')}]` : `[${tokens.allTools}]`
345
349
  const disabled = rule.enabled ? '' : ` (${tokens.disabled})`
350
+ const sourcePart = source !== undefined && source.length > 0 ? ` [${tokens.src}:${source}]` : ''
346
351
  const description = rule.description !== undefined ? ` (${truncate(rule.description, DISPLAY_TRUNCATE)})` : ''
347
352
  const tags = rule.tags.length > 0 ? ` [${tokens.tags}:${rule.tags.join(',')}]` : ''
348
- return `${rule.index + 1}. ${rule.action}${disabled} ${match}: ${truncate(rule.reason, DISPLAY_TRUNCATE)}${description}${tags}`
353
+ return `${rule.index + 1}. ${rule.action}${disabled} ${match}${sourcePart}: ${truncate(rule.reason, DISPLAY_TRUNCATE)}${description}${tags}`
349
354
  }
350
355
 
351
356
  /** The localized dimension-prefix vocabulary {@link describeRule} renders. */
@@ -360,6 +365,8 @@ export interface DescribeTokens {
360
365
  readonly platform: string
361
366
  readonly disabled: string
362
367
  readonly tags: string
368
+ /** Prefix of the per-rule source-file attribution. */
369
+ readonly src: string
363
370
  }
364
371
 
365
372
  /** Cut a display string at `limit` characters, marking truncation with `…`. */