Merge remote-tracking branch 'origin/master' into codex/trim-ai-prose

# Conflicts:
#	packages/ui/acp/tests/stream-update.spec.ts
This commit is contained in:
Tianyi Cui
2026-07-14 00:08:53 +08:00
11 changed files with 244 additions and 71 deletions

14
.jscpd.json Normal file
View File

@@ -0,0 +1,14 @@
{
"minTokens": 100,
"minLines": 10,
"mode": "mild",
"format": ["typescript"],
"pattern": "**/src/**/*.ts",
"ignorePattern": [
"(?s)/\\* jscpd:ignore-start \\*/.*?/\\* jscpd:ignore-end \\*/"
],
"reporters": ["console"],
"exitCode": 1,
"noColors": true,
"noTips": true
}

View File

@@ -1,4 +1,5 @@
import stylistic from '@stylistic/eslint-plugin'
import sonarjs from 'eslint-plugin-sonarjs'
import tseslint from 'typescript-eslint'
// Strict type-aware correctness rules plus repository formatting. Tests/examples relax deliberate
@@ -105,6 +106,17 @@ export default tseslint.config(
},
},
// --- file-local duplication (all owned TypeScript) ---------------------
{
files: ['packages/**/*.ts', 'examples/**/*.ts', 'scripts/**/*.ts'],
plugins: { sonarjs },
rules: {
// Cross-file clones are covered separately by jscpd.
'sonarjs/no-identical-functions': 'error',
'sonarjs/no-duplicated-branches': 'error',
},
},
// --- formatting (everything we own) -------------------------------------
{
files: ['packages/**/*.ts', 'examples/**/*.ts', 'scripts/**/*.ts', 'eslint.config.mjs'],

View File

@@ -17,6 +17,7 @@
"typecheck": "tsc -b tsconfig.json",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"duplication": "jscpd --config .jscpd.json packages",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"test:e2e": "vitest run --config vitest.e2e.config.ts",
@@ -81,7 +82,9 @@
"@types/node": "^22.20.0",
"@vitest/coverage-v8": "^4.1.8",
"eslint": "^10.4.1",
"eslint-plugin-sonarjs": "^4.1.0",
"fast-check": "^4.8.0",
"jscpd": "^5.0.12",
"jsdom": "29.1.1",
"knip": "^6.16.1",
"lefthook": "^2.1.9",

View File

@@ -98,16 +98,7 @@ async function callUntilText(
throw new Error(`${name} output did not include ${JSON.stringify(expected)}; last text was ${JSON.stringify(last !== undefined ? text(last) : '')}`)
}
class LossyReadBashExecutor extends BashExecutor {
private readonly task: BashTask = {
id: BashTaskId('bash-lossy'),
command: 'fake',
status: 'running',
exitCode: null,
signal: null,
done: Promise.resolve(),
}
abstract class TestBashExecutor extends BashExecutor {
resolve(request: BashExecRequest): BashExecSpec {
return {
command: request.command,
@@ -118,6 +109,17 @@ class LossyReadBashExecutor extends BashExecutor {
sandboxMode: request.sandboxMode,
}
}
}
class LossyReadBashExecutor extends TestBashExecutor {
private readonly task: BashTask = {
id: BashTaskId('bash-lossy'),
command: 'fake',
status: 'running',
exitCode: null,
signal: null,
done: Promise.resolve(),
}
run(): Promise<BashRunResult> {
return Promise.reject(new Error('not used'))
@@ -1029,7 +1031,7 @@ describe('sandbox rendering', () => {
// it anyway: an executor that reports no sandboxMode (fields never
// advertised) whose task nonetheless carries denial facts must render
// the marker without suggesting a lever the schema does not offer.
class FactsOnlyExecutor extends BashExecutor {
class FactsOnlyExecutor extends TestBashExecutor {
private readonly task: BashTask = {
id: BashTaskId('bash-facts'),
command: 'fake',
@@ -1040,17 +1042,6 @@ describe('sandbox rendering', () => {
sandbox: { mode: 'read-only', denied: true },
}
resolve(request: BashExecRequest): BashExecSpec {
return {
command: request.command,
workdir: request.workdir ?? process.cwd(),
timeoutMs: request.timeoutMs ?? 0,
...request.signal ? { signal: request.signal } : {},
owner: request.owner,
sandboxMode: request.sandboxMode,
}
}
run(): Promise<BashRunResult> { return Promise.reject(new Error('not used')) }
start(): BashTask { return this.task }
get(id: string): BashTask | undefined { return id === this.task.id ? this.task : undefined }

View File

@@ -46,6 +46,9 @@ export function parseCodexConfig(raw: unknown): ParsedCodexConfig {
for (const event of CODEX_EVENTS) {
const rawGroups = hooksMap[event]
// Matcher-group parsing remains dialect-local because the supported hook
// shapes and skip reasons differ from Claude Code's.
/* jscpd:ignore-start */
if (!Array.isArray(rawGroups)) continue
const groups: MatcherGroup[] = []
for (const rawGroup of rawGroups) {
@@ -57,6 +60,7 @@ export function parseCodexConfig(raw: unknown): ParsedCodexConfig {
if (!hook) continue
const type = typeof hook.type === 'string' ? hook.type : 'command'
if (type !== 'command') { skipped.push({ event, reason: `unsupported "${type}" hook` }); continue }
/* jscpd:ignore-end */
if (hook.async === true) { skipped.push({ event, reason: 'async hook' }); continue }
if (typeof hook.command !== 'string') continue
// Codex accepts `timeout` or the `timeoutSec` alias.

View File

@@ -136,6 +136,9 @@ export function apply(ctx: Context, config: Config): void {
output.additionalContext = output.stdout
}
outputs.push(output)
// Execution and decision mapping remain in each bridge so dialect
// differences stay explicit at their owning seam.
/* jscpd:ignore-start */
if (output.systemMessage !== undefined) {
ctx.logger.warn(`hooks-codex: ${point} hook emitted a systemMessage, which is not yet surfaced (ignored)`)
}
@@ -171,12 +174,14 @@ export function apply(ctx: Context, config: Config): void {
if (context) agent.inject(context.content, { source: context.source })
})
.catch((error: unknown) => { ctx.logger.warn(`hooks-codex: SessionStart hook failed: ${String(error)}`) }))
/* jscpd:ignore-end */
})
// UserPromptSubmit → PromptDecision. Codex can only BLOCK (no allow/ask).
ctx.on('agent/prompt-submit', async (agent, content, _source, next): Promise<PromptDecision> => {
const turn = lastTurn(agent)
const merged = await runPoint('UserPromptSubmit', '', { ...turnBase(agent, 'UserPromptSubmit', model), prompt: blocksToText(content) }, { agent, turn, plainStdoutAsContext: true })
/* jscpd:ignore-start */
if (merged.decision === 'deny') return { kind: 'block', reason: merged.reason ?? 'blocked by UserPromptSubmit hook' }
// Context alone is not a veto: DELEGATE so a later prompt-submit listener can
// still block/rewrite, then fold our context onto its decision.
@@ -194,6 +199,7 @@ export function apply(ctx: Context, config: Config): void {
ctx.on('tools/pre-execute', async (exec, next): Promise<PreToolDecision> => {
const turn = lastTurn(exec.agent)
const merged = await runPoint('PreToolUse', exec.name, preToolPayload(exec, model), { ...exec.agent ? { agent: exec.agent } : {}, turn, ...exec.signal ? { signal: exec.signal } : {} })
/* jscpd:ignore-end */
if (merged.decision === 'deny') return { kind: 'deny', reason: merged.reason ?? 'blocked by PreToolUse hook' }
return next()
})
@@ -201,6 +207,7 @@ export function apply(ctx: Context, config: Config): void {
// PostToolUse → PostToolDecision (block with feedback, or attach context).
ctx.on('tools/post-execute', async (exec, result, next): Promise<PostToolDecision> => {
const turn = lastTurn(exec.agent)
/* jscpd:ignore-start */
const merged = await runPoint('PostToolUse', exec.name, postToolPayload(exec, result, model), { ...exec.agent ? { agent: exec.agent } : {}, turn, ...exec.signal ? { signal: exec.signal } : {} })
const context = contextFrom(merged)
if (merged.decision === 'deny') {
@@ -226,6 +233,7 @@ export function apply(ctx: Context, config: Config): void {
// loop-guard (stop_hook_active + a max-consecutive cap) is deferred.
ctx.on('agent/turn-continuation', async (agent, turn, _default, next): Promise<ContinuationDecision> => {
const merged = await runPoint('Stop', '', { ...turnBase(agent, 'Stop', model), stop_hook_active: false, last_assistant_message: null }, { agent, turn })
/* jscpd:ignore-end */
if (merged.decision === 'deny') {
// A blocking Stop hook forces continuation; a block with no reason (exit 2,
// empty stderr) still forces it — fall back to a generic steering line
@@ -240,6 +248,9 @@ export function apply(ctx: Context, config: Config): void {
// --- Codex DIALECT payloads: snake_case, model on every event, turn_id on
// turn-scoped events. ---
// These small payload helpers intentionally remain next to the dialect shape;
// sharing them would pull bridge-only agent/LLM dependencies into hook-protocol.
/* jscpd:ignore-start */
function lastTurn(agent: Agent | undefined): number {
if (!agent) return 0
const last = [...agent.session.events].findLast(e => e.type === 'turn/start')
@@ -252,6 +263,7 @@ function lastTurn(agent: Agent | undefined): number {
function blocksToText(content: ContentBlock[]): string {
return content.filter((b): b is Extract<ContentBlock, { type: 'text' }> => b.type === 'text').map(b => b.text).join('')
}
/* jscpd:ignore-end */
/** Base fields on every Codex payload (no turn_id). */
function base(agent: Agent | undefined, event: string, model: string): Record<string, unknown> {

View File

@@ -73,6 +73,9 @@ export class SessionPersistenceJsonl extends SessionPersistence implements Persi
this.coordinator = new PersistenceCoordinator<number>(this.ctx, this)
}
// Each backend keeps the typed service surface beside its storage hooks;
// extracting these trivial forwards would add an inheritance seam.
/* jscpd:ignore-start */
// --- SessionPersistence service surface (delegated to the coordinator) ---
create(meta: SessionHeader): Promise<void> {
@@ -97,6 +100,7 @@ export class SessionPersistenceJsonl extends SessionPersistence implements Persi
get inits(): Map<Session, Promise<void>> {
return this.coordinator.inits
}
/* jscpd:ignore-end */
// --- PersistenceBackend hooks (the file-bytes storage primitives) ---

View File

@@ -54,6 +54,16 @@ function chunkEvent(seq: number, turn: number, step: number, chunk: StreamChunk)
let dir: string
let file: string
/** Write a session log file and return its path. */
function writeSession(filename: string, header: { id: string; createdAt: number }, calls: StreamChunk[][]): string {
let seq = 1
const events: SessionEvent[] = []
calls.forEach((chunks, step) => { for (const c of chunks) events.push(chunkEvent(seq++, 1, step + 1, c)) })
const path = join(dir, filename)
writeFileSync(path, sessionJsonl(events, header), 'utf8')
return path
}
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), 'llm-replay-spec-'))
file = join(dir, 'session.jsonl')
@@ -391,16 +401,6 @@ describe('parseSessionHeader', () => {
})
describe('loadSessionScripts', () => {
/** Write a session log file and return its path. */
function writeSession(filename: string, header: { id: string; createdAt: number }, calls: StreamChunk[][]): string {
let seq = 1
const events: SessionEvent[] = []
calls.forEach((chunks, step) => { for (const c of chunks) events.push(chunkEvent(seq++, 1, step + 1, c)) })
const path = join(dir, filename)
writeFileSync(path, sessionJsonl(events, header), 'utf8')
return path
}
it('returns one primary script for a single-session scenario', () => {
const f = writeSession('session.jsonl', { id: 'p', createdAt: 100 }, [TEXT_CHUNKS])
const scripts: SessionScript[] = loadSessionScripts({ file: f })
@@ -502,16 +502,6 @@ describe('installLlmReplay (per-session keying)', () => {
{ type: 'finish', reason: { kind: 'stop' } },
]
/** Write a session log file and return its path. */
function writeSession(filename: string, header: { id: string; createdAt: number }, calls: StreamChunk[][]): string {
let seq = 1
const events: SessionEvent[] = []
calls.forEach((chunks, step) => { for (const c of chunks) events.push(chunkEvent(seq++, 1, step + 1, c)) })
const path = join(dir, filename)
writeFileSync(path, sessionJsonl(events, header), 'utf8')
return path
}
const live = (id: string): GenerateOptions =>
({ model: 'm', messages: [], sessionId: id as NonNullable<GenerateOptions['sessionId']> })

View File

@@ -30,6 +30,21 @@ function registryOf(...tools: ToolDefinition[]): Pick<ToolRegistryType, 'get'> {
return { get: name => map.get(name) }
}
function updatesWith(presenter: ToolPresenter, ...events: SessionEvent[]): SessionNotification['update'][] {
const out: SessionNotification['update'][] = []
for (const event of events) streamSessionEventUpdate(SessionId('s1'), event, n => out.push(n.update), presenter)
return out
}
async function fsCtx(): Promise<Context> {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(FsLocal)
await ctx.plugin(ToolFs)
return ctx
}
function evt<T extends SessionEvent['type']>(type: T, data: Extract<SessionEvent, { type: T }>['data']): SessionEvent {
return { type, seq: 0, time: 0, data } as SessionEvent
}
@@ -181,12 +196,6 @@ describe('ToolPresenter (tool-owned presentation via the tool registry)', () =>
}),
}
function updatesWith(presenter: ToolPresenter, ...events: SessionEvent[]): SessionNotification['update'][] {
const out: SessionNotification['update'][] = []
for (const event of events) streamSessionEventUpdate(SessionId('s1'), event, n => out.push(n.update), presenter)
return out
}
it('tool/call uses the tool: description→title, command→rawInput, tool kind', () => {
const presenter = new ToolPresenter(registryOf(bashLike))
const [update] = updatesWith(presenter, evt('tool/call', {
@@ -624,21 +633,6 @@ describe('result-time diff card (REAL fs edit tool → tool_call_update diff blo
// call-time snippet, then the tool/result carries the tool's computed applied-hunk `meta`,
// which presentResult narrows into a `diff` result card the bridge forwards as `{ type:
// 'diff' }` content blocks. The real tool is required because its result metadata is the contract.
async function fsCtx(): Promise<Context> {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(FsLocal)
await ctx.plugin(ToolFs)
return ctx
}
function updatesWith(presenter: ToolPresenter, ...events: SessionEvent[]): SessionNotification['update'][] {
const out: SessionNotification['update'][] = []
for (const event of events) streamSessionEventUpdate(SessionId('s1'), event, n => out.push(n.update), presenter)
return out
}
it('forwards the applied-hunk meta onto the wire as tool_call_update diff content', async () => {
const ctx = await fsCtx()
const presenter = new ToolPresenter(ctx.tools)
@@ -730,14 +724,6 @@ describe('relative-path display titles (bridge relativizes the title against the
// The bridge relativizes a file card's TITLE against the session workspace cwd (mirroring the
// reference adapter's `toDisplayPath`), while leaving location/diff paths raw. Use real fs tools
// and the absolute paths an editor supplies; presentation itself is args-only and lacks cwd.
async function fsCtx(): Promise<Context> {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(FsLocal)
await ctx.plugin(ToolFs)
return ctx
}
function callUpdate(ctx: Context, sessionCwd: string | undefined, name: string, args: unknown): SessionNotification['update'] {
const presenter = new ToolPresenter(ctx.tools)
const out: SessionNotification['update'][] = []

154
pnpm-lock.yaml generated
View File

@@ -29,9 +29,15 @@ importers:
eslint:
specifier: ^10.4.1
version: 10.5.0(jiti@2.7.0)
eslint-plugin-sonarjs:
specifier: ^4.1.0
version: 4.1.0(eslint@10.5.0(jiti@2.7.0))
fast-check:
specifier: ^4.8.0
version: 4.8.0
jscpd:
specifier: ^5.0.12
version: 5.0.12
jsdom:
specifier: 29.1.1
version: 29.1.1
@@ -3050,6 +3056,14 @@ packages:
buffer-equal-constant-time@1.0.1:
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
builtin-modules@3.3.0:
resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==}
engines: {node: '>=6'}
bytes@3.1.2:
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
engines: {node: '>= 0.8'}
cac@7.0.0:
resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==}
engines: {node: '>=20.19.0'}
@@ -3356,6 +3370,11 @@ packages:
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
engines: {node: '>=12'}
eslint-plugin-sonarjs@4.1.0:
resolution: {integrity: sha512-rh+FlVz0yfd2RNIb6WqSkuGh0addX/Qi5scwQ5FphXDFrM6fZKcxP1+attJ78yUKcyYfiu6MTaISPpAFPzqRJw==}
peerDependencies:
eslint: ^8.0.0 || ^9.0.0 || ^10.0.0
eslint-scope@9.1.2:
resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
@@ -3481,6 +3500,9 @@ packages:
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
functional-red-black-tree@1.0.1:
resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==}
gaxios@7.1.5:
resolution: {integrity: sha512-5FZy72Rh8LhtjmvDrKkI+lVhrsQrVKVsItxMoDm5mNQE+xR0WVIIs+jzPSJgBvKVsLi24fZhXJIsNI0bihDzFg==}
engines: {node: '>=18'}
@@ -3500,6 +3522,10 @@ packages:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'}
globals@17.7.0:
resolution: {integrity: sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==}
engines: {node: '>=18'}
globrex@0.1.2:
resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
@@ -3606,6 +3632,44 @@ packages:
resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==}
hasBin: true
jscpd-darwin-arm64@5.0.12:
resolution: {integrity: sha512-CQCDYhQY9yOGGeauzkRGYW/QtXurPCjfDkEhUoM/M5UdmpzxfG3i9mnNsaheJ0RdFRf73mL7JaLVRP8U2l5/kA==}
cpu: [arm64]
os: [darwin]
jscpd-darwin-x64@5.0.12:
resolution: {integrity: sha512-7XNCDfChP9fPkIWLepd0zGnTov7/5mR7rJ5Utc1MdWFdgkN//qz6FAJ1FhQA0E/qjFedy9XTucfUCgLybSBsjw==}
cpu: [x64]
os: [darwin]
jscpd-linux-arm64-gnu@5.0.12:
resolution: {integrity: sha512-i/usHD0/8twzb2Qo4vFWcnuAD4IDLS6K/mTXwYy1CiJZDw4gmjfH45jtjdKUyoutTAiNs+ZWZgx1gIRljUbFuA==}
cpu: [arm64]
os: [linux]
libc: [glibc]
jscpd-linux-x64-gnu@5.0.12:
resolution: {integrity: sha512-TqaeSTaGawcqyXSrQvYJFtLRn4aQeHgphGGsq2ZtVAg+lPeuMmh81c3bl0yi2dL3gDX1RGBQKBVul1XQknOuoA==}
cpu: [x64]
os: [linux]
libc: [glibc]
jscpd-linux-x64-musl@5.0.12:
resolution: {integrity: sha512-WelugY1/rdoo0Y0sqJ2XQOIvyIZTfRe+vP3MJe4XvEUwPF0v+9SQoS34FnfblRhsddVixyNkgl7x/sHid9UMJw==}
cpu: [x64]
os: [linux]
libc: [musl]
jscpd-windows-x64-msvc@5.0.12:
resolution: {integrity: sha512-01x1klKjvfzI6Of5tI9u72x7TIQZQLLG6kMdsEPJKl/BXbskdknrfCepeTXRBbdFPKs25jfcd0klZ4OdDvww5w==}
cpu: [x64]
os: [win32]
jscpd@5.0.12:
resolution: {integrity: sha512-87dC+akj2mCywlt8p3xnRlDg0B55u4GDbfE9cuwOOeIxbEuWuIoNqGoYi65A0516aJ4EzAjGwBj1Qb/Ahc8WAQ==}
engines: {node: '>=18'}
hasBin: true
jsdom@29.1.1:
resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0}
@@ -3636,6 +3700,10 @@ packages:
json-stable-stringify-without-jsonify@1.0.1:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
jsx-ast-utils-x@0.1.0:
resolution: {integrity: sha512-eQQBjBnsVtGacsG9uJNB8qOr3yA8rga4wAaGG1qRcBzSIvfhERLrWxMAM1hp5fcS6Abo8M4+bUBTekYR0qTPQw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
jwa@2.0.1:
resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==}
@@ -3802,6 +3870,9 @@ packages:
lodash-es@4.18.1:
resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==}
lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
long@5.3.2:
resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==}
@@ -4107,6 +4178,14 @@ packages:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'}
refa@0.12.1:
resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
regexp-ast-analysis@0.7.1:
resolution: {integrity: sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
require-from-string@2.0.2:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
@@ -4173,6 +4252,10 @@ packages:
schemastery@3.18.0:
resolution: {integrity: sha512-Jw2uxjoyyqc/yeurmChUEc/jbi8GsrdXV/KmqRUDZXJAXAmrJiPsz8vKa17l/VckyzljHZ9oGaul443CQiXxtA==}
scslre@0.3.0:
resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==}
engines: {node: ^14.0.0 || >=16.0.0}
semver@7.8.4:
resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==}
engines: {node: '>=10'}
@@ -5855,6 +5938,10 @@ snapshots:
buffer-equal-constant-time@1.0.1: {}
builtin-modules@3.3.0: {}
bytes@3.1.2: {}
cac@7.0.0: {}
ccount@2.0.1: {}
@@ -6186,6 +6273,23 @@ snapshots:
escape-string-regexp@5.0.0: {}
eslint-plugin-sonarjs@4.1.0(eslint@10.5.0(jiti@2.7.0)):
dependencies:
'@eslint-community/regexpp': 4.12.2
builtin-modules: 3.3.0
bytes: 3.1.2
eslint: 10.5.0(jiti@2.7.0)
functional-red-black-tree: 1.0.1
globals: 17.7.0
jsx-ast-utils-x: 0.1.0
lodash.merge: 4.6.2
minimatch: 10.2.5
scslre: 0.3.0
semver: 7.8.4
ts-api-utils: 2.5.0(typescript@6.0.3)
typescript: 6.0.3
yaml: 2.9.0
eslint-scope@9.1.2:
dependencies:
'@types/esrecurse': 4.3.1
@@ -6330,6 +6434,8 @@ snapshots:
fsevents@2.3.3:
optional: true
functional-red-black-tree@1.0.1: {}
gaxios@7.1.5:
dependencies:
extend: 3.0.2
@@ -6358,6 +6464,8 @@ snapshots:
dependencies:
is-glob: 4.0.3
globals@17.7.0: {}
globrex@0.1.2: {}
google-auth-library@10.7.0:
@@ -6452,6 +6560,33 @@ snapshots:
dependencies:
argparse: 2.0.1
jscpd-darwin-arm64@5.0.12:
optional: true
jscpd-darwin-x64@5.0.12:
optional: true
jscpd-linux-arm64-gnu@5.0.12:
optional: true
jscpd-linux-x64-gnu@5.0.12:
optional: true
jscpd-linux-x64-musl@5.0.12:
optional: true
jscpd-windows-x64-msvc@5.0.12:
optional: true
jscpd@5.0.12:
optionalDependencies:
jscpd-darwin-arm64: 5.0.12
jscpd-darwin-x64: 5.0.12
jscpd-linux-arm64-gnu: 5.0.12
jscpd-linux-x64-gnu: 5.0.12
jscpd-linux-x64-musl: 5.0.12
jscpd-windows-x64-msvc: 5.0.12
jsdom@29.1.1:
dependencies:
'@asamuzakjp/css-color': 5.1.11
@@ -6495,6 +6630,8 @@ snapshots:
json-stable-stringify-without-jsonify@1.0.1: {}
jsx-ast-utils-x@0.1.0: {}
jwa@2.0.1:
dependencies:
buffer-equal-constant-time: 1.0.1
@@ -6639,6 +6776,8 @@ snapshots:
lodash-es@4.18.1: {}
lodash.merge@4.6.2: {}
long@5.3.2: {}
longest-streak@3.1.0: {}
@@ -7155,6 +7294,15 @@ snapshots:
readdirp@4.1.2: {}
refa@0.12.1:
dependencies:
'@eslint-community/regexpp': 4.12.2
regexp-ast-analysis@0.7.1:
dependencies:
'@eslint-community/regexpp': 4.12.2
refa: 0.12.1
require-from-string@2.0.2: {}
resolve-pkg-maps@1.0.0: {}
@@ -7247,6 +7395,12 @@ snapshots:
'@standard-schema/spec': 1.1.0
cosmokit: 1.8.1
scslre@0.3.0:
dependencies:
'@eslint-community/regexpp': 4.12.2
refa: 0.12.1
regexp-ast-analysis: 0.7.1
semver@7.8.4: {}
shebang-command@2.0.0:

View File

@@ -129,6 +129,7 @@ function gatesForMode(selected: Mode): Gate[] {
case 'ci-lint':
return [
lintGate(),
pnpmScript('duplication', 'duplication'),
]
case 'ci-coverage':
return [
@@ -152,6 +153,7 @@ function gatesForMode(selected: Mode): Gate[] {
case 'pre-push':
return [
pnpmScript('test', 'test'),
pnpmScript('duplication', 'duplication'),
pnpmScript('snapshot', 'test:snapshot'),
pnpmScript('build', 'build'),
...hygieneLeafGates({ artifactNeeds: ['build'] }),
@@ -166,6 +168,7 @@ function ciPrimaryGates(): Gate[] {
pnpmScript('constraints', 'constraints'),
pnpmScript('typecheck', 'typecheck'),
lintGate(),
pnpmScript('duplication', 'duplication'),
coverageGate(),
pnpmScript('snapshot', 'test:snapshot'),
demoSmokeGate({ needs: ['lint'] }),