@wringer/core 0.1.1 → 0.2.0
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/README.md +20 -13
- package/dist/build-change-request.d.ts +8 -0
- package/dist/create-wringer.d.ts +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/is-glob-match.d.ts +1 -0
- package/dist/pick-submodule-refusal.d.ts +1 -1
- package/dist/read-changed-paths.d.ts +2 -6
- package/dist/read-fingerprint.d.ts +2 -5
- package/dist/report-schema.d.ts +1 -0
- package/dist/types.d.ts +1 -0
- package/package.json +3 -3
- package/src/build-change-request.ts +18 -0
- package/src/build-report.ts +12 -8
- package/src/builtins/build-verdict-commands.ts +48 -22
- package/src/builtins/start-run.ts +5 -5
- package/src/create-wringer.ts +5 -0
- package/src/dispatch.ts +5 -5
- package/src/index.ts +2 -0
- package/src/is-glob-match.ts +5 -0
- package/src/pick-submodule-refusal.ts +8 -2
- package/src/read-changed-paths.ts +31 -13
- package/src/read-fingerprint.ts +105 -8
- package/src/render-report-markdown.ts +25 -1
- package/src/report-schema.ts +4 -0
- package/src/types.ts +4 -0
package/README.md
CHANGED
|
@@ -39,6 +39,7 @@ export const registry = createWringer({ name: 'shop' })
|
|
|
39
39
|
}),
|
|
40
40
|
})
|
|
41
41
|
.feature({ name: 'checkout', summary: 'Pay for the cart', paths: ['src/checkout/**'] })
|
|
42
|
+
.ignore('docs/**', '**/*.md', '**/*.test.ts')
|
|
42
43
|
.build();
|
|
43
44
|
```
|
|
44
45
|
|
|
@@ -53,22 +54,28 @@ own file.
|
|
|
53
54
|
- Read-only targets: each driver client and exec refuses a write there with `read-only` and the
|
|
54
55
|
target's hint. A target's driver config lists the writes it grants.
|
|
55
56
|
- One supervisor process per run, which owns the app and every driver service.
|
|
56
|
-
- The built-in commands, from `up` and `doctor` to `done`, `report` and `gate`.
|
|
57
|
+
- The built-in commands, from `up` and `doctor` to `done`, `report` and `gate`. `gate` fails a PR
|
|
58
|
+
when a changed feature lacks a verdict at the current fingerprint, and when a changed path matches
|
|
59
|
+
neither a feature's `paths` nor an `.ignore` glob. `report` lists those unmapped paths.
|
|
60
|
+
- The ignore list: `.ignore(...globs)` names the paths with no runtime effect, such as docs and
|
|
61
|
+
tests. Any path an ignore glob matches counts as no change, even when a feature's `paths` match it
|
|
62
|
+
too. The gate skips it and the fingerprint leaves it out, so a README edit keeps each verdict
|
|
63
|
+
fresh. Feature `paths` and ignore globs both match with `Bun.Glob`.
|
|
57
64
|
- The envelope's zod schema, and the JSON Schema built from it.
|
|
58
65
|
- A closed set of error codes, each mapped to one exit code.
|
|
59
66
|
- Run state on disk: `state.json` holds pointers, and each run's `log.ndjson` holds its events.
|
|
60
|
-
- The fingerprint: `git patch-id --verbatim` of the working tree against the merge base,
|
|
61
|
-
the same across a commit and across a clean rebase
|
|
62
|
-
they were. A rebase that changes those lines changes
|
|
63
|
-
diff's context. It needs git 2.39 or later. Only the
|
|
64
|
-
the config, attributes and ignore files of the clone,
|
|
65
|
-
same change gets the same fingerprint in any clone. A
|
|
66
|
-
adds it, such as `core.autocrlf` or a clean filter, can
|
|
67
|
-
committed; the verdict then reads as stale, never as
|
|
68
|
-
a shallow or partial clone and a detached HEAD get
|
|
69
|
-
clone, and a file on disk that the index marks
|
|
70
|
-
|
|
71
|
-
committed fail with an `environment` error.
|
|
67
|
+
- The fingerprint: `git patch-id --verbatim` of the working tree against the merge base, less the
|
|
68
|
+
state folder and the ignored paths, which stays the same across a commit and across a clean rebase
|
|
69
|
+
that leaves the 3 lines around each change as they were. A rebase that changes those lines changes
|
|
70
|
+
the fingerprint, because the hash keeps the diff's context. It needs git 2.39 or later. Only the
|
|
71
|
+
tracked files and git's defaults shape it: the config, attributes and ignore files of the clone,
|
|
72
|
+
the user and the system never apply, so the same change gets the same fingerprint in any clone. A
|
|
73
|
+
clone whose config converts a file as git adds it, such as `core.autocrlf` or a clean filter, can
|
|
74
|
+
get another fingerprint once the change is committed; the verdict then reads as stale, never as
|
|
75
|
+
fresh. A sparse checkout, a linked worktree, a shallow or partial clone and a detached HEAD get
|
|
76
|
+
the fingerprint of the same change in a full clone, and a file on disk that the index marks
|
|
77
|
+
skip-worktree counts as any other. A repo with no commit, a shallow clone that lacks the merge
|
|
78
|
+
base, and a submodule that holds work it has not committed fail with an `environment` error.
|
|
72
79
|
- Two hosts for exec: `local()`, and `ssh()` with an argv prefix in layers, quoted once per shell
|
|
73
80
|
layer. Each host also follows a long-running command, such as `tail -F`, line by line.
|
|
74
81
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Wringer } from './types.ts';
|
|
2
|
+
export interface ChangeRequest {
|
|
3
|
+
readonly cwd: string;
|
|
4
|
+
readonly base: string;
|
|
5
|
+
readonly exclude: readonly string[];
|
|
6
|
+
readonly ignore: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
export declare function buildChangeRequest(wringer: Pick<Wringer, 'stateDir' | 'ignore'>, cwd: string, base: string): ChangeRequest;
|
package/dist/create-wringer.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface WringerBuilder<Ds extends AnyDrivers> {
|
|
|
10
10
|
readonly action: <Input, Data>(action: ActionDefinition<Input, Data, Ds>) => WringerBuilder<Ds>;
|
|
11
11
|
readonly check: <Input, Data>(check: CheckDefinition<Input, Data, Ds>) => WringerBuilder<Ds>;
|
|
12
12
|
readonly feature: (...features: readonly FeatureDefinition[]) => WringerBuilder<Ds>;
|
|
13
|
+
readonly ignore: (...globs: readonly string[]) => WringerBuilder<Ds>;
|
|
13
14
|
readonly hook: <Name extends keyof HookHandlers>(name: Name, handler: HookHandlers[Name]) => WringerBuilder<Ds>;
|
|
14
15
|
readonly build: () => Wringer;
|
|
15
16
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { bindDrivers } from './bind-drivers.ts';
|
|
2
2
|
export type { BoundHelpers } from './bind-drivers.ts';
|
|
3
|
+
export { buildBuiltinCommand } from './build-commands.ts';
|
|
3
4
|
export { buildEnvelope } from './build-envelope.ts';
|
|
4
5
|
export type { EnvelopeInput } from './build-envelope.ts';
|
|
5
6
|
export { buildEnvelopeJSONSchema } from './build-envelope-json-schema.ts';
|
|
@@ -16,6 +17,7 @@ export type { StateFileSystem } from './create-node-state-file-system.ts';
|
|
|
16
17
|
export { createWringer, defaultStateDir } from './create-wringer.ts';
|
|
17
18
|
export type { WringerBuilder, WringerOptions } from './create-wringer.ts';
|
|
18
19
|
export { defineDriver } from './define-driver.ts';
|
|
20
|
+
export { prepareArgs } from './prepare-args.ts';
|
|
19
21
|
export { findOwnValue } from './find-own-value.ts';
|
|
20
22
|
export type { DriverDefinition, DriverFactory } from './define-driver.ts';
|
|
21
23
|
export { dataCapBytes } from './cap-data.ts';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function isGlobMatch(path: string, globs: readonly string[]): boolean;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { WringerError } from './wringer-error.ts';
|
|
2
|
-
export declare function pickSubmoduleRefusal(statusEntries: Uint8Array): WringerError | null;
|
|
2
|
+
export declare function pickSubmoduleRefusal(statusEntries: Uint8Array, ignore: readonly string[]): WringerError | null;
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
+
import type { ChangeRequest } from './build-change-request.ts';
|
|
1
2
|
import type { FingerprintHost } from './read-fingerprint.ts';
|
|
2
|
-
export
|
|
3
|
-
readonly cwd: string;
|
|
4
|
-
readonly base: string;
|
|
5
|
-
readonly exclude: readonly string[];
|
|
6
|
-
}
|
|
7
|
-
export declare function readChangedPaths(host: Pick<FingerprintHost, 'runGit'>, request: ChangedPathsRequest): Promise<readonly string[]>;
|
|
3
|
+
export declare function readChangedPaths(host: Pick<FingerprintHost, 'runGit' | 'runGitBytes'>, request: ChangeRequest): Promise<readonly string[]>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ChangeRequest } from './build-change-request.ts';
|
|
1
2
|
import type { GitRequest } from './run-git.ts';
|
|
2
3
|
import type { ProcessResult } from './run-process.ts';
|
|
3
4
|
export interface FingerprintHost {
|
|
@@ -5,9 +6,5 @@ export interface FingerprintHost {
|
|
|
5
6
|
readonly runGitBytes: (request: GitRequest) => Promise<Uint8Array>;
|
|
6
7
|
readonly removeDirectory: (path: string) => Promise<void>;
|
|
7
8
|
}
|
|
8
|
-
export
|
|
9
|
-
readonly cwd: string;
|
|
10
|
-
readonly base: string;
|
|
11
|
-
readonly exclude: readonly string[];
|
|
12
|
-
}
|
|
9
|
+
export type FingerprintRequest = ChangeRequest;
|
|
13
10
|
export declare function readFingerprint(host: FingerprintHost, request: FingerprintRequest): Promise<string>;
|
package/dist/report-schema.d.ts
CHANGED
|
@@ -46,6 +46,7 @@ export declare const reportSchema: z.ZodObject<{
|
|
|
46
46
|
}, z.core.$strict>>;
|
|
47
47
|
}, z.core.$strict>>;
|
|
48
48
|
}, z.core.$strict>>;
|
|
49
|
+
unmapped: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
49
50
|
}, z.core.$strict>;
|
|
50
51
|
export type Report = z.infer<typeof reportSchema>;
|
|
51
52
|
export {};
|
package/dist/types.d.ts
CHANGED
|
@@ -317,6 +317,7 @@ export interface Wringer {
|
|
|
317
317
|
readonly commands: ReadonlyMap<string, RegisteredCommand>;
|
|
318
318
|
readonly checks: readonly RunnableRepoCheck[];
|
|
319
319
|
readonly features: readonly FeatureDefinition[];
|
|
320
|
+
readonly ignore: readonly string[];
|
|
320
321
|
readonly hooks: Partial<HookHandlers>;
|
|
321
322
|
}
|
|
322
323
|
export interface SupervisorStartRequest {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wringer/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "The core of wringer: the envelope, the registry, the fence, the run state and the evidence",
|
|
5
5
|
"homepage": "https://github.com/zgeoff/wringer/tree/main/packages/core#readme",
|
|
6
6
|
"bugs": "https://github.com/zgeoff/wringer/issues",
|
|
@@ -35,13 +35,13 @@
|
|
|
35
35
|
"schema": "bun scripts/write-envelope-schema.ts"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@zgeoff/std": "0.4.
|
|
38
|
+
"@zgeoff/std": "0.4.3",
|
|
39
39
|
"ts-pattern": "5.9.2",
|
|
40
40
|
"xstate": "5.33.3",
|
|
41
41
|
"zod": "4.6.7"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@zgeoff/testing": "0.
|
|
44
|
+
"@zgeoff/testing": "0.3.0",
|
|
45
45
|
"ajv": "8.20.0",
|
|
46
46
|
"tiny-invariant": "1.3.3"
|
|
47
47
|
},
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Wringer } from './types.ts';
|
|
2
|
+
|
|
3
|
+
export interface ChangeRequest {
|
|
4
|
+
readonly cwd: string;
|
|
5
|
+
readonly base: string;
|
|
6
|
+
readonly exclude: readonly string[];
|
|
7
|
+
readonly ignore: readonly string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Every read of the fingerprint or the changed paths leaves out the same paths, so a verdict, a run
|
|
11
|
+
// and the gate agree on what the branch changes.
|
|
12
|
+
export function buildChangeRequest(
|
|
13
|
+
wringer: Pick<Wringer, 'stateDir' | 'ignore'>,
|
|
14
|
+
cwd: string,
|
|
15
|
+
base: string,
|
|
16
|
+
): ChangeRequest {
|
|
17
|
+
return { cwd, base, exclude: [wringer.stateDir], ignore: wringer.ignore };
|
|
18
|
+
}
|
package/src/build-report.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isGlobMatch } from './is-glob-match.ts';
|
|
1
2
|
import type { FeatureStatus, Report, Verdict } from './report-schema.ts';
|
|
2
3
|
import type { FeatureDefinition } from './types.ts';
|
|
3
4
|
|
|
@@ -10,13 +11,11 @@ export interface ReportInput {
|
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
// Each feature whose paths the branch changes needs a verdict at the current fingerprint. A feature
|
|
13
|
-
// the branch leaves alone is `unchanged`, whatever its verdict.
|
|
14
|
+
// the branch leaves alone is `unchanged`, whatever its verdict. A changed path no feature maps is
|
|
15
|
+
// unmapped, and leaves the report incomplete.
|
|
14
16
|
export function buildReport(input: ReportInput): Report {
|
|
15
17
|
const features = input.features.map((feature) => {
|
|
16
|
-
const changed = input.changedPaths.some((path) =>
|
|
17
|
-
feature.paths.some((pattern) => new Bun.Glob(pattern).match(path)),
|
|
18
|
-
);
|
|
19
|
-
|
|
18
|
+
const changed = input.changedPaths.some((path) => isGlobMatch(path, feature.paths));
|
|
20
19
|
const verdict = input.verdicts.get(feature.name) ?? null;
|
|
21
20
|
|
|
22
21
|
return {
|
|
@@ -27,14 +26,19 @@ export function buildReport(input: ReportInput): Report {
|
|
|
27
26
|
};
|
|
28
27
|
});
|
|
29
28
|
|
|
29
|
+
const unmapped = input.changedPaths.filter(
|
|
30
|
+
(path) => !input.features.some((feature) => isGlobMatch(path, feature.paths)),
|
|
31
|
+
);
|
|
32
|
+
|
|
30
33
|
return {
|
|
31
34
|
version: 1,
|
|
32
35
|
base: input.base,
|
|
33
36
|
fingerprint: input.fingerprint,
|
|
34
|
-
complete:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
complete:
|
|
38
|
+
unmapped.length === 0 &&
|
|
39
|
+
features.every((feature) => feature.status === 'verified' || feature.status === 'unchanged'),
|
|
37
40
|
features,
|
|
41
|
+
unmapped,
|
|
38
42
|
};
|
|
39
43
|
}
|
|
40
44
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isAbsolute, join } from 'node:path';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
+
import { buildChangeRequest } from '../build-change-request.ts';
|
|
3
4
|
import { buildBuiltinCommand } from '../build-commands.ts';
|
|
4
5
|
import { buildReport } from '../build-report.ts';
|
|
5
6
|
import type { Evidence } from '../envelope.ts';
|
|
@@ -38,6 +39,7 @@ const gateFailureSchema = z.strictObject({ name: z.string(), reason: z.string()
|
|
|
38
39
|
const gateDataSchema = z.strictObject({
|
|
39
40
|
fingerprint: z.string(),
|
|
40
41
|
failing: z.array(gateFailureSchema),
|
|
42
|
+
unmapped: z.array(z.string()),
|
|
41
43
|
});
|
|
42
44
|
|
|
43
45
|
type GateData = z.infer<typeof gateDataSchema>;
|
|
@@ -47,7 +49,8 @@ type GateData = z.infer<typeof gateDataSchema>;
|
|
|
47
49
|
export function buildVerdictCommands(): readonly RegisteredCommand[] {
|
|
48
50
|
const done: BuiltinCommandSpec<DoneArgs, Verdict> = {
|
|
49
51
|
path: [doneCommand],
|
|
50
|
-
summary:
|
|
52
|
+
summary:
|
|
53
|
+
'Record a verdict for a feature from its commands that touched the app since the doctor passed',
|
|
51
54
|
args: doneArgsSchema,
|
|
52
55
|
data: verdictSchema,
|
|
53
56
|
effect: 'read',
|
|
@@ -75,7 +78,7 @@ export function buildVerdictCommands(): readonly RegisteredCommand[] {
|
|
|
75
78
|
const gate: BuiltinCommandSpec<GateArgs, GateData> = {
|
|
76
79
|
path: ['gate'],
|
|
77
80
|
summary:
|
|
78
|
-
'Fail when a changed feature lacks a verdict at the current fingerprint in the PR body',
|
|
81
|
+
'Fail when a changed feature lacks a verdict at the current fingerprint in the PR body, or a changed path maps to no feature',
|
|
79
82
|
args: gateArgsSchema,
|
|
80
83
|
data: gateDataSchema,
|
|
81
84
|
effect: 'read',
|
|
@@ -120,7 +123,7 @@ async function recordVerdict(args: DoneArgs, ctx: BuiltinContext): Promise<Verdi
|
|
|
120
123
|
|
|
121
124
|
if (drives.length === 0) {
|
|
122
125
|
throw new WringerError('check-failed', {
|
|
123
|
-
message: `No
|
|
126
|
+
message: `No command of ${feature} that touches the app passed on run ${ctx.run.runID} since the doctor passed.`,
|
|
124
127
|
hint: `Drive the feature with \`--feature ${feature}\`, then run \`done\` again.`,
|
|
125
128
|
});
|
|
126
129
|
}
|
|
@@ -140,11 +143,10 @@ async function recordVerdict(args: DoneArgs, ctx: BuiltinContext): Promise<Verdi
|
|
|
140
143
|
}
|
|
141
144
|
|
|
142
145
|
function readCurrentFingerprint(ctx: BuiltinContext, base: string): Promise<string> {
|
|
143
|
-
return readFingerprint(
|
|
144
|
-
|
|
145
|
-
base,
|
|
146
|
-
|
|
147
|
-
});
|
|
146
|
+
return readFingerprint(
|
|
147
|
+
ctx.session.repoHost,
|
|
148
|
+
buildChangeRequest(ctx.wringer, ctx.session.root, base),
|
|
149
|
+
);
|
|
148
150
|
}
|
|
149
151
|
|
|
150
152
|
async function readReport(ctx: BuiltinContext, givenBase: string | undefined): Promise<Report> {
|
|
@@ -152,11 +154,7 @@ async function readReport(ctx: BuiltinContext, givenBase: string | undefined): P
|
|
|
152
154
|
|
|
153
155
|
const [fingerprint, changedPaths, verdicts] = await Promise.all([
|
|
154
156
|
readCurrentFingerprint(ctx, base),
|
|
155
|
-
readChangedPaths(ctx.session.repoHost,
|
|
156
|
-
cwd: ctx.session.root,
|
|
157
|
-
base,
|
|
158
|
-
exclude: [ctx.wringer.stateDir],
|
|
159
|
-
}),
|
|
157
|
+
readChangedPaths(ctx.session.repoHost, buildChangeRequest(ctx.wringer, ctx.session.root, base)),
|
|
160
158
|
readVerdicts(ctx.state, ctx.session.repoHost, ctx.session.root),
|
|
161
159
|
]);
|
|
162
160
|
|
|
@@ -211,11 +209,10 @@ async function checkGate(args: GateArgs, ctx: BuiltinContext): Promise<GateData>
|
|
|
211
209
|
|
|
212
210
|
const [fingerprint, changedPaths] = await Promise.all([
|
|
213
211
|
readCurrentFingerprint(ctx, args.base),
|
|
214
|
-
readChangedPaths(
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}),
|
|
212
|
+
readChangedPaths(
|
|
213
|
+
ctx.session.repoHost,
|
|
214
|
+
buildChangeRequest(ctx.wringer, ctx.session.root, args.base),
|
|
215
|
+
),
|
|
219
216
|
]);
|
|
220
217
|
|
|
221
218
|
const claimed = block.ok
|
|
@@ -251,15 +248,44 @@ async function checkGate(args: GateArgs, ctx: BuiltinContext): Promise<GateData>
|
|
|
251
248
|
return [];
|
|
252
249
|
});
|
|
253
250
|
|
|
254
|
-
const data = { fingerprint, failing };
|
|
251
|
+
const data = { fingerprint, failing, unmapped: computed.unmapped };
|
|
255
252
|
|
|
256
|
-
if (failing.length > 0) {
|
|
253
|
+
if (failing.length > 0 || computed.unmapped.length > 0) {
|
|
257
254
|
throw new WringerError('check-failed', {
|
|
258
|
-
message:
|
|
259
|
-
hint:
|
|
255
|
+
message: formatGateFailure(failing, computed.unmapped),
|
|
256
|
+
hint: pickGateHint(failing, computed.unmapped),
|
|
260
257
|
data,
|
|
261
258
|
});
|
|
262
259
|
}
|
|
263
260
|
|
|
264
261
|
return data;
|
|
265
262
|
}
|
|
263
|
+
|
|
264
|
+
function formatGateFailure(
|
|
265
|
+
failing: readonly { readonly name: string }[],
|
|
266
|
+
unmapped: readonly string[],
|
|
267
|
+
): string {
|
|
268
|
+
const lines = [
|
|
269
|
+
failing.length > 0
|
|
270
|
+
? `${failing.length} changed feature(s) lack a verdict at the current fingerprint: ${failing.map((feature) => feature.name).join(', ')}.`
|
|
271
|
+
: null,
|
|
272
|
+
unmapped.length > 0
|
|
273
|
+
? `${unmapped.length} changed path(s) map to no feature: ${unmapped.join(', ')}.`
|
|
274
|
+
: null,
|
|
275
|
+
];
|
|
276
|
+
|
|
277
|
+
return lines.filter((line) => line !== null).join(' ');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const verdictHint = 'Drive each feature, run `done` and `report --human`, and update the PR body.';
|
|
281
|
+
|
|
282
|
+
const unmappedHint =
|
|
283
|
+
"Add each unmapped path to a feature's `paths`, or to the registry's ignore list when it has no runtime effect.";
|
|
284
|
+
|
|
285
|
+
function pickGateHint(failing: readonly unknown[], unmapped: readonly string[]): string {
|
|
286
|
+
if (unmapped.length === 0) {
|
|
287
|
+
return verdictHint;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return failing.length === 0 ? unmappedHint : `${unmappedHint} ${verdictHint}`;
|
|
291
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { buildChangeRequest } from '../build-change-request.ts';
|
|
2
3
|
import type { RunView } from '../build-run-view.ts';
|
|
3
4
|
import { findOwnValue } from '../find-own-value.ts';
|
|
4
5
|
import { readDefaultBase } from '../read-default-base.ts';
|
|
@@ -52,11 +53,10 @@ export function startRun(ctx: BuiltinContext, request: StartRunRequest): Promise
|
|
|
52
53
|
|
|
53
54
|
const base = await readDefaultBase(ctx.session.repoHost, ctx.session.root);
|
|
54
55
|
|
|
55
|
-
const fingerprint = await readFingerprint(
|
|
56
|
-
|
|
57
|
-
base,
|
|
58
|
-
|
|
59
|
-
});
|
|
56
|
+
const fingerprint = await readFingerprint(
|
|
57
|
+
ctx.session.repoHost,
|
|
58
|
+
buildChangeRequest(ctx.wringer, ctx.session.root, base),
|
|
59
|
+
);
|
|
60
60
|
|
|
61
61
|
// The caller has its timeout already once the deadline passes, so no run starts after it.
|
|
62
62
|
if (ctx.signal.aborted) {
|
package/src/create-wringer.ts
CHANGED
|
@@ -33,6 +33,7 @@ export interface WringerBuilder<Ds extends AnyDrivers> {
|
|
|
33
33
|
readonly action: <Input, Data>(action: ActionDefinition<Input, Data, Ds>) => WringerBuilder<Ds>;
|
|
34
34
|
readonly check: <Input, Data>(check: CheckDefinition<Input, Data, Ds>) => WringerBuilder<Ds>;
|
|
35
35
|
readonly feature: (...features: readonly FeatureDefinition[]) => WringerBuilder<Ds>;
|
|
36
|
+
readonly ignore: (...globs: readonly string[]) => WringerBuilder<Ds>;
|
|
36
37
|
readonly hook: <Name extends keyof HookHandlers>(
|
|
37
38
|
name: Name,
|
|
38
39
|
handler: HookHandlers[Name],
|
|
@@ -50,6 +51,7 @@ export function createWringer(options: WringerOptions): WringerBuilder<readonly
|
|
|
50
51
|
commands: [],
|
|
51
52
|
checks: [],
|
|
52
53
|
features: [],
|
|
54
|
+
ignore: [],
|
|
53
55
|
hooks: {},
|
|
54
56
|
});
|
|
55
57
|
}
|
|
@@ -61,6 +63,7 @@ interface Parts {
|
|
|
61
63
|
readonly commands: readonly ((driverNames: readonly string[]) => RegisteredCommand)[];
|
|
62
64
|
readonly checks: readonly ((driverNames: readonly string[]) => RunnableRepoCheck)[];
|
|
63
65
|
readonly features: readonly FeatureDefinition[];
|
|
66
|
+
readonly ignore: readonly string[];
|
|
64
67
|
readonly hooks: Partial<HookHandlers>;
|
|
65
68
|
}
|
|
66
69
|
|
|
@@ -83,6 +86,7 @@ function makeBuilder<Ds extends AnyDrivers>(parts: Parts): WringerBuilder<Ds> {
|
|
|
83
86
|
check: (check) =>
|
|
84
87
|
makeBuilder({ ...parts, checks: [...parts.checks, (names) => buildRepoCheck(check, names)] }),
|
|
85
88
|
feature: (...features) => makeBuilder({ ...parts, features: [...parts.features, ...features] }),
|
|
89
|
+
ignore: (...globs) => makeBuilder({ ...parts, ignore: [...parts.ignore, ...globs] }),
|
|
86
90
|
hook: (name, handler) => makeBuilder({ ...parts, hooks: { ...parts.hooks, [name]: handler } }),
|
|
87
91
|
build: () => buildWringer(parts),
|
|
88
92
|
};
|
|
@@ -147,6 +151,7 @@ function buildWringer(parts: Parts): Wringer {
|
|
|
147
151
|
),
|
|
148
152
|
checks: Object.freeze(parts.checks.map((buildEntry) => buildEntry(driverNames))),
|
|
149
153
|
features: Object.freeze([...parts.features]),
|
|
154
|
+
ignore: Object.freeze([...parts.ignore]),
|
|
150
155
|
hooks: Object.freeze({ ...parts.hooks }),
|
|
151
156
|
});
|
|
152
157
|
}
|
package/src/dispatch.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { match } from 'ts-pattern';
|
|
3
|
+
import { buildChangeRequest } from './build-change-request.ts';
|
|
3
4
|
import { buildEnvelope } from './build-envelope.ts';
|
|
4
5
|
import type { RunView } from './build-run-view.ts';
|
|
5
6
|
import { buildRunPaths } from './build-state-paths.ts';
|
|
@@ -367,11 +368,10 @@ async function runCommand(request: CommandRequest): Promise<CommandOutcome> {
|
|
|
367
368
|
async function probeFingerprintChanged(request: CommandRequest): Promise<boolean> {
|
|
368
369
|
const base = await readDefaultBase(request.session.repoHost, request.session.root);
|
|
369
370
|
|
|
370
|
-
const fingerprint = await readFingerprint(
|
|
371
|
-
|
|
372
|
-
base,
|
|
373
|
-
|
|
374
|
-
});
|
|
371
|
+
const fingerprint = await readFingerprint(
|
|
372
|
+
request.session.repoHost,
|
|
373
|
+
buildChangeRequest(request.wringer, request.session.root, base),
|
|
374
|
+
);
|
|
375
375
|
|
|
376
376
|
return fingerprint !== request.context.run?.fingerprint;
|
|
377
377
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { bindDrivers } from './bind-drivers.ts';
|
|
2
2
|
export type { BoundHelpers } from './bind-drivers.ts';
|
|
3
|
+
export { buildBuiltinCommand } from './build-commands.ts';
|
|
3
4
|
export { buildEnvelope } from './build-envelope.ts';
|
|
4
5
|
export type { EnvelopeInput } from './build-envelope.ts';
|
|
5
6
|
export { buildEnvelopeJSONSchema } from './build-envelope-json-schema.ts';
|
|
@@ -16,6 +17,7 @@ export type { StateFileSystem } from './create-node-state-file-system.ts';
|
|
|
16
17
|
export { createWringer, defaultStateDir } from './create-wringer.ts';
|
|
17
18
|
export type { WringerBuilder, WringerOptions } from './create-wringer.ts';
|
|
18
19
|
export { defineDriver } from './define-driver.ts';
|
|
20
|
+
export { prepareArgs } from './prepare-args.ts';
|
|
19
21
|
export { findOwnValue } from './find-own-value.ts';
|
|
20
22
|
export type { DriverDefinition, DriverFactory } from './define-driver.ts';
|
|
21
23
|
export { dataCapBytes } from './cap-data.ts';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Every glob matches through Bun's matcher, never as a git pathspec, whose `*` and `**` differ, so
|
|
2
|
+
// the changed paths, the fingerprint and the report agree on which paths a glob covers.
|
|
3
|
+
export function isGlobMatch(path: string, globs: readonly string[]): boolean {
|
|
4
|
+
return globs.some((glob) => new Bun.Glob(glob).match(path));
|
|
5
|
+
}
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
import { isGlobMatch } from './is-glob-match.ts';
|
|
1
2
|
import { WringerError } from './wringer-error.ts';
|
|
2
3
|
|
|
3
4
|
// `status --porcelain=v2` gives each submodule a field `S<c><m><u>`: `m` is `M` when its tracked
|
|
4
5
|
// files changed and `u` is `U` when it holds untracked files. A moved commit alone reaches the
|
|
5
6
|
// fingerprint through the gitlink, but work inside a submodule never does.
|
|
6
|
-
export function pickSubmoduleRefusal(
|
|
7
|
-
|
|
7
|
+
export function pickSubmoduleRefusal(
|
|
8
|
+
statusEntries: Uint8Array,
|
|
9
|
+
ignore: readonly string[],
|
|
10
|
+
): WringerError | null {
|
|
11
|
+
const dirty = collectDirtySubmodules(Buffer.from(statusEntries).toString('utf8')).filter(
|
|
12
|
+
(path) => !isGlobMatch(path, ignore),
|
|
13
|
+
);
|
|
8
14
|
|
|
9
15
|
if (dirty.length === 0) {
|
|
10
16
|
return null;
|
|
@@ -1,16 +1,12 @@
|
|
|
1
|
+
import type { ChangeRequest } from './build-change-request.ts';
|
|
2
|
+
import { isGlobMatch } from './is-glob-match.ts';
|
|
1
3
|
import type { FingerprintHost } from './read-fingerprint.ts';
|
|
2
4
|
|
|
3
|
-
export interface ChangedPathsRequest {
|
|
4
|
-
readonly cwd: string;
|
|
5
|
-
readonly base: string;
|
|
6
|
-
readonly exclude: readonly string[];
|
|
7
|
-
}
|
|
8
|
-
|
|
9
5
|
// The paths the branch changes against its merge base: committed, staged and unstaged edits, and
|
|
10
|
-
// untracked files that git does not ignore.
|
|
6
|
+
// untracked files that git does not ignore. A path an ignore glob matches is left out.
|
|
11
7
|
export async function readChangedPaths(
|
|
12
|
-
host: Pick<FingerprintHost, 'runGit'>,
|
|
13
|
-
request:
|
|
8
|
+
host: Pick<FingerprintHost, 'runGit' | 'runGitBytes'>,
|
|
9
|
+
request: ChangeRequest,
|
|
14
10
|
): Promise<readonly string[]> {
|
|
15
11
|
const mergeBaseResult = await host.runGit({
|
|
16
12
|
cwd: request.cwd,
|
|
@@ -21,19 +17,41 @@ export async function readChangedPaths(
|
|
|
21
17
|
const excludes = request.exclude.map((path) => `:(exclude)${path}`);
|
|
22
18
|
|
|
23
19
|
// `-z` gives each path as it is: without it, git quotes a path with a tab, a newline or a
|
|
24
|
-
// non-ASCII letter, and the quoted form matches no feature's glob.
|
|
20
|
+
// non-ASCII letter, and the quoted form matches no feature's glob. Each path is decoded on its
|
|
21
|
+
// own, as the fingerprint decodes it, so a leading byte-order mark stays part of the name.
|
|
25
22
|
const [tracked, untracked] = await Promise.all([
|
|
26
|
-
host.
|
|
23
|
+
host.runGitBytes({
|
|
27
24
|
cwd: request.cwd,
|
|
28
25
|
args: ['diff', '--name-only', '-z', '--no-renames', mergeBase, '--', '.', ...excludes],
|
|
29
26
|
}),
|
|
30
|
-
host.
|
|
27
|
+
host.runGitBytes({
|
|
31
28
|
cwd: request.cwd,
|
|
32
29
|
args: ['ls-files', '-z', '--others', '--exclude-standard', '--', '.', ...excludes],
|
|
33
30
|
}),
|
|
34
31
|
]);
|
|
35
32
|
|
|
36
|
-
const paths =
|
|
33
|
+
const paths = [...splitNames(tracked), ...splitNames(untracked)].filter(
|
|
34
|
+
(path) => !isGlobMatch(path, request.ignore),
|
|
35
|
+
);
|
|
37
36
|
|
|
38
37
|
return [...new Set(paths)].toSorted();
|
|
39
38
|
}
|
|
39
|
+
|
|
40
|
+
function splitNames(output: Uint8Array): readonly string[] {
|
|
41
|
+
const names: string[] = [];
|
|
42
|
+
const bytes = Buffer.from(output);
|
|
43
|
+
let start = 0;
|
|
44
|
+
|
|
45
|
+
while (start < bytes.length) {
|
|
46
|
+
const end = bytes.indexOf(0, start);
|
|
47
|
+
const stop = end === -1 ? bytes.length : end;
|
|
48
|
+
|
|
49
|
+
if (stop > start) {
|
|
50
|
+
names.push(bytes.subarray(start, stop).toString('utf8'));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
start = stop + 1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return names;
|
|
57
|
+
}
|
package/src/read-fingerprint.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
|
+
import type { ChangeRequest } from './build-change-request.ts';
|
|
3
4
|
import { buildPromisedObjectReads } from './build-promised-object-reads.ts';
|
|
4
5
|
import { buildSkipWorktreeMarks } from './build-skip-worktree-marks.ts';
|
|
5
6
|
import { buildSubmoduleAdds } from './build-submodule-adds.ts';
|
|
7
|
+
import { isGlobMatch } from './is-glob-match.ts';
|
|
6
8
|
import { parsePatchID } from './parse-patch-id.ts';
|
|
7
9
|
import { pickMergeBaseFailure } from './pick-merge-base-failure.ts';
|
|
8
10
|
import { pickMergeBaseSteps } from './pick-merge-base-steps.ts';
|
|
@@ -17,11 +19,9 @@ export interface FingerprintHost {
|
|
|
17
19
|
readonly removeDirectory: (path: string) => Promise<void>;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
readonly exclude: readonly string[];
|
|
24
|
-
}
|
|
22
|
+
// `ignore` holds globs of paths with no runtime effect: a change to a path one matches leaves the
|
|
23
|
+
// fingerprint alone.
|
|
24
|
+
export type FingerprintRequest = ChangeRequest;
|
|
25
25
|
|
|
26
26
|
const pinnedDiffOptions = [
|
|
27
27
|
'--unified=3',
|
|
@@ -46,7 +46,7 @@ export async function readFingerprint(
|
|
|
46
46
|
const mergeBase = await readMergeBase(host, root, request.base);
|
|
47
47
|
|
|
48
48
|
await readPromisedObjects(host, root, mergeBase);
|
|
49
|
-
await readSubmoduleWork(host, root);
|
|
49
|
+
await readSubmoduleWork(host, root, request.ignore);
|
|
50
50
|
|
|
51
51
|
const gitDir = await readGitPath(host, root, `wringer-${randomUUID()}`);
|
|
52
52
|
|
|
@@ -75,6 +75,7 @@ export async function readFingerprint(
|
|
|
75
75
|
});
|
|
76
76
|
|
|
77
77
|
await addSubmodules(host, root, env);
|
|
78
|
+
await restoreIgnoredPaths(host, { root, env, mergeBase, ignore: request.ignore });
|
|
78
79
|
|
|
79
80
|
// The exclusions apply to the diff, not to `add`: `add` refuses an ignored path in a pathspec,
|
|
80
81
|
// and a committed change to an excluded path reaches the index from the repo's own anyway.
|
|
@@ -156,7 +157,11 @@ async function readPromisedObjects(
|
|
|
156
157
|
|
|
157
158
|
// `--no-optional-locks` keeps status from writing the repo's own index, and `-uno` skips the
|
|
158
159
|
// repo's own untracked files, which `add --all` counts anyway.
|
|
159
|
-
async function readSubmoduleWork(
|
|
160
|
+
async function readSubmoduleWork(
|
|
161
|
+
host: FingerprintHost,
|
|
162
|
+
root: string,
|
|
163
|
+
ignore: readonly string[],
|
|
164
|
+
): Promise<void> {
|
|
160
165
|
const status = await host.runGitBytes({
|
|
161
166
|
cwd: root,
|
|
162
167
|
args: [
|
|
@@ -169,7 +174,7 @@ async function readSubmoduleWork(host: FingerprintHost, root: string): Promise<v
|
|
|
169
174
|
],
|
|
170
175
|
});
|
|
171
176
|
|
|
172
|
-
const refusal = pickSubmoduleRefusal(status);
|
|
177
|
+
const refusal = pickSubmoduleRefusal(status, ignore);
|
|
173
178
|
|
|
174
179
|
if (refusal !== null) {
|
|
175
180
|
throw refusal;
|
|
@@ -269,3 +274,95 @@ async function addSubmodules(
|
|
|
269
274
|
await host.runGit(request);
|
|
270
275
|
}
|
|
271
276
|
}
|
|
277
|
+
|
|
278
|
+
interface IgnoredPathsRequest {
|
|
279
|
+
readonly root: string;
|
|
280
|
+
readonly env: Readonly<Record<string, string>>;
|
|
281
|
+
readonly mergeBase: string;
|
|
282
|
+
readonly ignore: readonly string[];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Each changed path an ignore glob matches goes back to its merge-base state in the index, so the
|
|
286
|
+
// diff leaves it out. The names go to git as the bytes it listed, on stdin, so a name that is not
|
|
287
|
+
// UTF-8 still matches, no count of names outgrows the argument list, and `*` stays a plain letter.
|
|
288
|
+
async function restoreIgnoredPaths(
|
|
289
|
+
host: FingerprintHost,
|
|
290
|
+
request: IgnoredPathsRequest,
|
|
291
|
+
): Promise<void> {
|
|
292
|
+
if (request.ignore.length === 0) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const names = await host.runGitBytes({
|
|
297
|
+
cwd: request.root,
|
|
298
|
+
args: [
|
|
299
|
+
'diff',
|
|
300
|
+
'--cached',
|
|
301
|
+
'--name-only',
|
|
302
|
+
'-z',
|
|
303
|
+
'--no-renames',
|
|
304
|
+
'--no-relative',
|
|
305
|
+
request.mergeBase,
|
|
306
|
+
],
|
|
307
|
+
env: request.env,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
const ignored = collectIgnoredNames(Buffer.from(names), request.ignore);
|
|
311
|
+
|
|
312
|
+
if (ignored.length === 0) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
await host.runGit({
|
|
317
|
+
cwd: request.root,
|
|
318
|
+
args: [
|
|
319
|
+
'--literal-pathspecs',
|
|
320
|
+
'restore',
|
|
321
|
+
'--staged',
|
|
322
|
+
`--source=${request.mergeBase}`,
|
|
323
|
+
'--pathspec-from-file=-',
|
|
324
|
+
'--pathspec-file-nul',
|
|
325
|
+
],
|
|
326
|
+
stdin: Buffer.concat(ignored.flatMap((name) => [name, Buffer.from([0])])),
|
|
327
|
+
env: request.env,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// A name matches as the changed paths decode it, and stays the bytes git wrote. An ignored name
|
|
332
|
+
// stays in the diff when a counted name sits inside it or around it, such as a file that became a
|
|
333
|
+
// folder: restoring one would remove the other from the index.
|
|
334
|
+
function collectIgnoredNames(output: Buffer, ignore: readonly string[]): readonly Buffer[] {
|
|
335
|
+
const names = collectNames(output);
|
|
336
|
+
const ignored = names.filter((name) => isGlobMatch(name.toString('utf8'), ignore));
|
|
337
|
+
const counted = names.filter((name) => !ignored.includes(name));
|
|
338
|
+
|
|
339
|
+
return ignored.filter(
|
|
340
|
+
(name) => !counted.some((other) => isInsidePath(other, name) || isInsidePath(name, other)),
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function collectNames(output: Buffer): readonly Buffer[] {
|
|
345
|
+
const names: Buffer[] = [];
|
|
346
|
+
let start = 0;
|
|
347
|
+
|
|
348
|
+
while (start < output.length) {
|
|
349
|
+
const end = output.indexOf(0, start);
|
|
350
|
+
const stop = end === -1 ? output.length : end;
|
|
351
|
+
|
|
352
|
+
if (stop > start) {
|
|
353
|
+
names.push(output.subarray(start, stop));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
start = stop + 1;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return names;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function isInsidePath(path: Buffer, folder: Buffer): boolean {
|
|
363
|
+
return (
|
|
364
|
+
path.length > folder.length &&
|
|
365
|
+
path[folder.length] === 0x2f &&
|
|
366
|
+
path.subarray(0, folder.length).equals(folder)
|
|
367
|
+
);
|
|
368
|
+
}
|
|
@@ -16,7 +16,7 @@ export function renderReportMarkdown(report: Report): string {
|
|
|
16
16
|
const lines = [
|
|
17
17
|
'## wringer report',
|
|
18
18
|
'',
|
|
19
|
-
`Fingerprint \`${report.fingerprint}\` against \`${report.base}\`: ${report
|
|
19
|
+
`Fingerprint \`${report.fingerprint}\` against \`${report.base}\`: ${pickSummary(report)}.`,
|
|
20
20
|
'',
|
|
21
21
|
'| Feature | Status | Evidence |',
|
|
22
22
|
'| --- | --- | --- |',
|
|
@@ -25,6 +25,7 @@ export function renderReportMarkdown(report: Report): string {
|
|
|
25
25
|
`| ${feature.name} | ${statusLabels[feature.status]} | ${formatEvidence(feature.verdict?.evidence ?? [])} |`,
|
|
26
26
|
),
|
|
27
27
|
'',
|
|
28
|
+
...formatUnmapped(report.unmapped),
|
|
28
29
|
reportBlockOpen,
|
|
29
30
|
JSON.stringify(report).replaceAll('>', String.raw`\u003e`),
|
|
30
31
|
reportBlockClose,
|
|
@@ -34,6 +35,16 @@ export function renderReportMarkdown(report: Report): string {
|
|
|
34
35
|
return lines.join('\n');
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
function pickSummary(report: Report): string {
|
|
39
|
+
if (report.complete) {
|
|
40
|
+
return 'every changed feature is verified';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return report.unmapped.length > 0
|
|
44
|
+
? 'some changed paths map to no feature'
|
|
45
|
+
: 'some changed features need a verdict';
|
|
46
|
+
}
|
|
47
|
+
|
|
37
48
|
function formatEvidence(
|
|
38
49
|
evidence: readonly {
|
|
39
50
|
readonly kind: string;
|
|
@@ -51,3 +62,16 @@ function formatEvidence(
|
|
|
51
62
|
)
|
|
52
63
|
.join(', ');
|
|
53
64
|
}
|
|
65
|
+
|
|
66
|
+
function formatUnmapped(unmapped: readonly string[]): readonly string[] {
|
|
67
|
+
if (unmapped.length === 0) {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return [
|
|
72
|
+
'Changed paths that no feature maps. Map each to a feature, or add it to the ignore list:',
|
|
73
|
+
'',
|
|
74
|
+
...unmapped.map((path) => `- \`${path}\``),
|
|
75
|
+
'',
|
|
76
|
+
];
|
|
77
|
+
}
|
package/src/report-schema.ts
CHANGED
|
@@ -32,6 +32,10 @@ export const reportSchema = z.strictObject({
|
|
|
32
32
|
fingerprint: z.string(),
|
|
33
33
|
complete: z.boolean(),
|
|
34
34
|
features: z.array(featureReportSchema),
|
|
35
|
+
|
|
36
|
+
// The changed paths that no feature maps and the repo does not ignore. A report written before
|
|
37
|
+
// the field existed reads as having none; `gate` works the paths out again either way.
|
|
38
|
+
unmapped: z.array(z.string()).default([]),
|
|
35
39
|
});
|
|
36
40
|
|
|
37
41
|
export type Report = z.infer<typeof reportSchema>;
|
package/src/types.ts
CHANGED
|
@@ -427,6 +427,10 @@ export interface Wringer {
|
|
|
427
427
|
readonly commands: ReadonlyMap<string, RegisteredCommand>;
|
|
428
428
|
readonly checks: readonly RunnableRepoCheck[];
|
|
429
429
|
readonly features: readonly FeatureDefinition[];
|
|
430
|
+
|
|
431
|
+
// Globs of paths with no runtime effect, such as docs: a change to one maps to no feature and
|
|
432
|
+
// leaves the fingerprint alone.
|
|
433
|
+
readonly ignore: readonly string[];
|
|
430
434
|
readonly hooks: Partial<HookHandlers>;
|
|
431
435
|
}
|
|
432
436
|
|