@ploaness/cli 1.0.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/LICENSE +21 -0
- package/README.md +13 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +161 -0
- package/dist/bin.js.map +1 -0
- package/dist/checks/assets.d.ts +30 -0
- package/dist/checks/assets.d.ts.map +1 -0
- package/dist/checks/assets.js +169 -0
- package/dist/checks/assets.js.map +1 -0
- package/dist/checks/containers.d.ts +9 -0
- package/dist/checks/containers.d.ts.map +1 -0
- package/dist/checks/containers.js +194 -0
- package/dist/checks/containers.js.map +1 -0
- package/dist/checks/conventions.d.ts +8 -0
- package/dist/checks/conventions.d.ts.map +1 -0
- package/dist/checks/conventions.js +77 -0
- package/dist/checks/conventions.js.map +1 -0
- package/dist/checks/dependencies.d.ts +11 -0
- package/dist/checks/dependencies.d.ts.map +1 -0
- package/dist/checks/dependencies.js +389 -0
- package/dist/checks/dependencies.js.map +1 -0
- package/dist/checks/editorconfig.d.ts +5 -0
- package/dist/checks/editorconfig.d.ts.map +1 -0
- package/dist/checks/editorconfig.js +38 -0
- package/dist/checks/editorconfig.js.map +1 -0
- package/dist/checks/generated.d.ts +10 -0
- package/dist/checks/generated.d.ts.map +1 -0
- package/dist/checks/generated.js +65 -0
- package/dist/checks/generated.js.map +1 -0
- package/dist/checks/history.d.ts +19 -0
- package/dist/checks/history.d.ts.map +1 -0
- package/dist/checks/history.js +71 -0
- package/dist/checks/history.js.map +1 -0
- package/dist/checks/images.d.ts +14 -0
- package/dist/checks/images.d.ts.map +1 -0
- package/dist/checks/images.js +100 -0
- package/dist/checks/images.js.map +1 -0
- package/dist/checks/install.d.ts +5 -0
- package/dist/checks/install.d.ts.map +1 -0
- package/dist/checks/install.js +21 -0
- package/dist/checks/install.js.map +1 -0
- package/dist/checks/integrity.d.ts +11 -0
- package/dist/checks/integrity.d.ts.map +1 -0
- package/dist/checks/integrity.js +66 -0
- package/dist/checks/integrity.js.map +1 -0
- package/dist/checks/payload.d.ts +7 -0
- package/dist/checks/payload.d.ts.map +1 -0
- package/dist/checks/payload.js +81 -0
- package/dist/checks/payload.js.map +1 -0
- package/dist/checks/preflight.d.ts +5 -0
- package/dist/checks/preflight.d.ts.map +1 -0
- package/dist/checks/preflight.js +48 -0
- package/dist/checks/preflight.js.map +1 -0
- package/dist/checks/references.d.ts +18 -0
- package/dist/checks/references.d.ts.map +1 -0
- package/dist/checks/references.js +100 -0
- package/dist/checks/references.js.map +1 -0
- package/dist/checks/suppressions.d.ts +5 -0
- package/dist/checks/suppressions.d.ts.map +1 -0
- package/dist/checks/suppressions.js +50 -0
- package/dist/checks/suppressions.js.map +1 -0
- package/dist/checks/tailwind.d.ts +5 -0
- package/dist/checks/tailwind.d.ts.map +1 -0
- package/dist/checks/tailwind.js +29 -0
- package/dist/checks/tailwind.js.map +1 -0
- package/dist/checks/tests.d.ts +8 -0
- package/dist/checks/tests.d.ts.map +1 -0
- package/dist/checks/tests.js +123 -0
- package/dist/checks/tests.js.map +1 -0
- package/dist/checks/toolchain.d.ts +25 -0
- package/dist/checks/toolchain.d.ts.map +1 -0
- package/dist/checks/toolchain.js +165 -0
- package/dist/checks/toolchain.js.map +1 -0
- package/dist/checks/tree.d.ts +6 -0
- package/dist/checks/tree.d.ts.map +1 -0
- package/dist/checks/tree.js +52 -0
- package/dist/checks/tree.js.map +1 -0
- package/dist/checks/wiring.d.ts +5 -0
- package/dist/checks/wiring.d.ts.map +1 -0
- package/dist/checks/wiring.js +163 -0
- package/dist/checks/wiring.js.map +1 -0
- package/dist/commands/commit-message.d.ts +11 -0
- package/dist/commands/commit-message.d.ts.map +1 -0
- package/dist/commands/commit-message.js +57 -0
- package/dist/commands/commit-message.js.map +1 -0
- package/dist/commands/format.d.ts +4 -0
- package/dist/commands/format.d.ts.map +1 -0
- package/dist/commands/format.js +35 -0
- package/dist/commands/format.js.map +1 -0
- package/dist/commands/hooks.d.ts +9 -0
- package/dist/commands/hooks.d.ts.map +1 -0
- package/dist/commands/hooks.js +57 -0
- package/dist/commands/hooks.js.map +1 -0
- package/dist/commands/init.d.ts +4 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +92 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/sync.d.ts +4 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +30 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/commands/verify.d.ts +18 -0
- package/dist/commands/verify.d.ts.map +1 -0
- package/dist/commands/verify.js +96 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/context.d.ts +157 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +345 -0
- package/dist/context.js.map +1 -0
- package/dist/exec.d.ts +80 -0
- package/dist/exec.d.ts.map +1 -0
- package/dist/exec.js +117 -0
- package/dist/exec.js.map +1 -0
- package/dist/gates.d.ts +61 -0
- package/dist/gates.d.ts.map +1 -0
- package/dist/gates.js +298 -0
- package/dist/gates.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/report.d.ts +40 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +224 -0
- package/dist/report.js.map +1 -0
- package/package.json +60 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
// Dependency gates. A license is a fixed fact of a package version, so it changes state only when you add
|
|
2
|
+
// or upgrade something: a true signal about your change. Freshness is bounded rather than banned, because
|
|
3
|
+
// "a newer version exists" is not a property of your change, but "the version I declared is two majors
|
|
4
|
+
// behind" is, and that is a real liability.
|
|
5
|
+
import { realpathSync } from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { asRecord, asText, collectCoordinates, findFreshnessViolations, findLicenseViolations, HARNESS_EXCEPTIONS, HARNESS_PACKAGE, hoursPublished, inheritedManifestPaths, isArray, isHeldByReleaseAge, isRecord, judgeVulnerabilities, mapWithConcurrency, NETWORKED_TOOL_TIMEOUT_MS, REGISTRY_CONCURRENCY, RELEASE_AGE_FLOOR_HOURS, REQUEST_TIMEOUT_MS, } from '@ploaness/governance';
|
|
8
|
+
import { manifestPathFrom, readJson, trackedFiles } from '../context.js';
|
|
9
|
+
import { failed, passed, run, TIMED_OUT_CODE, withOutput, } from '../exec.js';
|
|
10
|
+
import { imageFreshness } from './images.js';
|
|
11
|
+
/** Every dependency license must sit inside the allowlist. */
|
|
12
|
+
// Report enough of a tool's raw output to diagnose it, without pasting a whole log into a finding.
|
|
13
|
+
const MAX_REPORTED_CHARS = 2000;
|
|
14
|
+
const MAX_REPORTED_LINES = 5;
|
|
15
|
+
const MAX_LISTED_NAMES = 10;
|
|
16
|
+
// The registry answers with this status for a package it has never published.
|
|
17
|
+
const NOT_FOUND = 404;
|
|
18
|
+
export const licenses = (context) => {
|
|
19
|
+
const result = run('pnpm', ['licenses', 'list', '--json'], { cwd: context.root });
|
|
20
|
+
if (result.code !== 0) {
|
|
21
|
+
return failed('the license inventory could not be produced', [result.output]);
|
|
22
|
+
}
|
|
23
|
+
// `stdout`, not `output`. pnpm writes ` WARN ` lines to stderr as a matter of course, and `output`
|
|
24
|
+
// concatenates the two streams with no separator - so a warning about an unsupported engine made the
|
|
25
|
+
// inventory unparseable and this gate blamed the parse rather than the warning.
|
|
26
|
+
const grouped = parseLicenseInventory(result.stdout);
|
|
27
|
+
if (grouped === undefined) {
|
|
28
|
+
return failed('the license inventory was not valid JSON', [
|
|
29
|
+
result.output.slice(0, MAX_REPORTED_CHARS),
|
|
30
|
+
]);
|
|
31
|
+
}
|
|
32
|
+
const packages = Object.values(grouped)
|
|
33
|
+
.flat()
|
|
34
|
+
.map((entry) => ({
|
|
35
|
+
name: entry.name,
|
|
36
|
+
license: entry.license,
|
|
37
|
+
}));
|
|
38
|
+
const violations = findLicenseViolations(packages);
|
|
39
|
+
return withOutput(violations.length > 0
|
|
40
|
+
? failed(`${String(violations.length)} dependency license(s) outside policy`, violations.map((entry) => `${entry.name}: ${entry.license}`))
|
|
41
|
+
: passed(`all ${String(packages.length)} dependency licenses are within policy`), result.stdout);
|
|
42
|
+
};
|
|
43
|
+
const MANIFEST_NAME = 'package.json';
|
|
44
|
+
// Every manifest the repository tracks, not only the root one. A workspace declares its toolchain across
|
|
45
|
+
// several of them, and reading the root alone left every analyzer ploaness itself runs on unmeasured.
|
|
46
|
+
const trackedManifests = (context) => trackedFiles(context.root)
|
|
47
|
+
.filter((file) => file === MANIFEST_NAME || file.endsWith(`/${MANIFEST_NAME}`))
|
|
48
|
+
.map((file) => ({
|
|
49
|
+
path: file,
|
|
50
|
+
packageJson: readJson(path.join(context.root, file)),
|
|
51
|
+
isInherited: false,
|
|
52
|
+
}));
|
|
53
|
+
// A path as the filesystem finally names it, so the same manifest reached two ways compares equal.
|
|
54
|
+
// Under pnpm every installed package is a symlink into the store, and in a workspace it is a symlink
|
|
55
|
+
// back into the tree - which is how an inherited manifest can turn out to be a tracked one.
|
|
56
|
+
const realPathOrSelf = (file) => {
|
|
57
|
+
try {
|
|
58
|
+
return realpathSync(file);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return file;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
// The one place the walk touches the filesystem. Everything it decides - which dependencies to follow,
|
|
65
|
+
// how a diamond and a cycle are handled - is in @ploaness/governance, where a spec states it without
|
|
66
|
+
// building an install.
|
|
67
|
+
const HARNESS_RESOLVER = { locate: manifestPathFrom, read: readJson };
|
|
68
|
+
// A manifest names itself, which is what a finding should say. The path under node_modules is a store
|
|
69
|
+
// address that differs between two installs of the same version, so it identifies nothing a reader can
|
|
70
|
+
// act on.
|
|
71
|
+
const declaredName = (packageJson, fallback) => asText(asRecord(packageJson)['name']) || fallback;
|
|
72
|
+
// The manifests a project INHERITS: `ploaness` as the project itself resolves it, and every ploaness
|
|
73
|
+
// package that one pulls in. The standard counts these as declared coordinates, and they are the
|
|
74
|
+
// manifests that decide which analyzer versions the project's gates actually run - none of which the
|
|
75
|
+
// project tracks, so a reader of tracked files alone could never see them. That is why a consumer's
|
|
76
|
+
// update report was silent about a harness pin going stale while ploaness's own report was not.
|
|
77
|
+
const inheritedManifests = (context, tracked) => {
|
|
78
|
+
const alreadyRead = new Set(tracked.map((manifest) => realPathOrSelf(path.join(context.root, manifest.path))));
|
|
79
|
+
const entry = manifestPathFrom(HARNESS_PACKAGE, path.join(context.root, MANIFEST_NAME));
|
|
80
|
+
return inheritedManifestPaths(entry, HARNESS_RESOLVER)
|
|
81
|
+
.filter((file) => !alreadyRead.has(realPathOrSelf(file)))
|
|
82
|
+
.map((file) => {
|
|
83
|
+
const packageJson = readJson(file);
|
|
84
|
+
return {
|
|
85
|
+
path: `${declaredName(packageJson, file)}/${MANIFEST_NAME}`,
|
|
86
|
+
packageJson,
|
|
87
|
+
isInherited: true,
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
const manifestSources = (context) => {
|
|
92
|
+
const tracked = trackedManifests(context);
|
|
93
|
+
return [...tracked, ...inheritedManifests(context, tracked)];
|
|
94
|
+
};
|
|
95
|
+
// The registry exposes two documents per package: the packument root and, per version, a version
|
|
96
|
+
// document. `/{name}/latest` is the version document for the `latest` dist-tag and costs a few kilobytes,
|
|
97
|
+
// while the packument root costs tens of megabytes for a package as long-lived as `next`. Freshness needs
|
|
98
|
+
// one string, so the version document is the right request. It must NOT carry the abbreviated-packument
|
|
99
|
+
// accept type: that media type is defined only for the packument root, and the registry answers 406 for
|
|
100
|
+
// it here - inconsistently, because the CDN serves whichever representation it already holds.
|
|
101
|
+
const REGISTRY_ATTEMPTS = 3;
|
|
102
|
+
const versionDocumentUrl = (name) => `https://registry.npmjs.org/${encodeURIComponent(name).replace('%40', '@')}/latest`;
|
|
103
|
+
const parseLicenseInventory = (output) => {
|
|
104
|
+
try {
|
|
105
|
+
return JSON.parse(output);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
// A deadline on every attempt, not on the lookup as a whole. A registry that accepts the connection and
|
|
112
|
+
// then says nothing is the failure this catches, and it is the one an unbounded `fetch` waits out
|
|
113
|
+
// forever: `REGISTRY_ATTEMPTS` retries an inconclusive attempt, but an attempt that never concludes is
|
|
114
|
+
// never retried either. Three bounded attempts fail in a knowable time; one unbounded attempt does not
|
|
115
|
+
// fail at all.
|
|
116
|
+
/** One registry attempt: an answer, or undefined when the attempt was inconclusive and may be retried. */
|
|
117
|
+
const fetchOrUndefined = async (name) => {
|
|
118
|
+
try {
|
|
119
|
+
return await fetch(versionDocumentUrl(name), {
|
|
120
|
+
headers: { accept: 'application/json' },
|
|
121
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
const attemptLookup = async (name) => {
|
|
129
|
+
const response = await fetchOrUndefined(name);
|
|
130
|
+
if (response === undefined) {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
if (response.ok) {
|
|
134
|
+
const body = await response.json();
|
|
135
|
+
const version = asRecord(body)['version'];
|
|
136
|
+
return typeof version === 'string' ? { kind: 'version', version } : { kind: 'absent' };
|
|
137
|
+
}
|
|
138
|
+
// A 404 is a real answer, and retrying cannot change it.
|
|
139
|
+
return response.status === NOT_FOUND ? { kind: 'absent' } : undefined;
|
|
140
|
+
};
|
|
141
|
+
const latestVersion = async (name, attemptsLeft = REGISTRY_ATTEMPTS) => {
|
|
142
|
+
if (attemptsLeft <= 0) {
|
|
143
|
+
return { kind: 'unreachable' };
|
|
144
|
+
}
|
|
145
|
+
const lookup = await attemptLookup(name);
|
|
146
|
+
return lookup ?? (await latestVersion(name, attemptsLeft - 1));
|
|
147
|
+
};
|
|
148
|
+
const sortLookups = (coordinates, lookups) => {
|
|
149
|
+
const namesWhere = (kind) => [...lookups]
|
|
150
|
+
.filter(([, lookup]) => lookup.kind === kind)
|
|
151
|
+
.map(([name]) => name);
|
|
152
|
+
const statuses = coordinates.flatMap((coordinate) => {
|
|
153
|
+
const lookup = lookups.get(coordinate.name);
|
|
154
|
+
if (lookup?.kind !== 'version') {
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
return [{ ...coordinate, latest: lookup.version }];
|
|
158
|
+
});
|
|
159
|
+
return {
|
|
160
|
+
statuses,
|
|
161
|
+
unreachable: namesWhere('unreachable'),
|
|
162
|
+
unpublished: namesWhere('absent'),
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
// The version document carries no publication date, and the packument root that does costs tens of
|
|
166
|
+
// megabytes for a package as long-lived as `next`. The search document is a kilobyte and names the
|
|
167
|
+
// latest version beside its date, which is the only affordable answer to "how old is this release".
|
|
168
|
+
// It is asked ONLY for a coordinate the report already names - a handful, not every dependency - and
|
|
169
|
+
// every miss is silent: search matches by relevance rather than exactly, so a document naming another
|
|
170
|
+
// package or another version answers nothing, and an unknown date reports the update the ordinary way.
|
|
171
|
+
const searchDocumentUrl = (name) => `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(name)}&size=1`;
|
|
172
|
+
const publishedAt = async (name, version) => {
|
|
173
|
+
try {
|
|
174
|
+
const response = await fetch(searchDocumentUrl(name), {
|
|
175
|
+
headers: { accept: 'application/json' },
|
|
176
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
177
|
+
});
|
|
178
|
+
if (!response.ok) {
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
const objects = asRecord(await response.json())['objects'];
|
|
182
|
+
const first = isArray(objects) ? objects[0] : undefined;
|
|
183
|
+
const found = asRecord(asRecord(first)['package']);
|
|
184
|
+
if (found['name'] !== name || found['version'] !== version) {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
const date = found['date'];
|
|
188
|
+
const parsed = typeof date === 'string' ? Date.parse(date) : Number.NaN;
|
|
189
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
// The manifest leads the line because it is what the reader has to open. The standard asks the update
|
|
196
|
+
// report to name it for every coordinate, and a workspace pinning one analyzer in two places is exactly
|
|
197
|
+
// the case where the bare package name says nothing about where to make the change.
|
|
198
|
+
const describeFinding = (finding) => `${finding.owner} ${finding.name}: declared ${finding.current}, latest ${finding.latest}`;
|
|
199
|
+
// A line names its repair, and for an inherited coordinate that is not the repair every other line
|
|
200
|
+
// names. The project cannot edit a version ploaness declares, so "change the declaration" would be
|
|
201
|
+
// advice it has no file to act on; upgrading the harness is the move, and reporting it is the move when
|
|
202
|
+
// no release carries the newer pin yet. The same answer `HARNESS_EXCEPTIONS` gives for an advisory
|
|
203
|
+
// carried by ploaness's own chain.
|
|
204
|
+
const HARNESS_REPAIR = ' - declared by ploaness rather than by the project, so upgrade ploaness, or report it if no ' +
|
|
205
|
+
'release carries the newer pin';
|
|
206
|
+
// `stale`, not `update`, for a coordinate past the bound that does not stop this build. Printed as an
|
|
207
|
+
// ordinary update it would bury the one line saying the harness itself is overdue among a dozen saying
|
|
208
|
+
// a patch release exists.
|
|
209
|
+
const describeReported = (finding) => finding.verdict === 'fail'
|
|
210
|
+
? `stale ${describeFinding(finding)}; past the freshness bound${HARNESS_REPAIR}`
|
|
211
|
+
: `update ${describeFinding(finding)}`;
|
|
212
|
+
// `held`, not `update`, for a release pnpm will refuse for being too young. Naming it as an ordinary
|
|
213
|
+
// update sends the reader to an install that fails, and the way past that failure is an exclusion from
|
|
214
|
+
// the very guard the wait exists to keep. It says the age so the reader can see how short the wait is.
|
|
215
|
+
const describeHeld = (finding, hours) => {
|
|
216
|
+
const age = hours === undefined ? 'recently' : `${String(hours)}h ago`;
|
|
217
|
+
return (`held ${finding.owner} ${finding.name}: latest ${finding.latest}, published ${age}; below the ` +
|
|
218
|
+
`${String(RELEASE_AGE_FLOOR_HOURS)}h release-age floor pnpm enforces, so it installs once it ages`);
|
|
219
|
+
};
|
|
220
|
+
// A date is asked for only where it can change the line: a coordinate already past the bound is stale
|
|
221
|
+
// whatever its age, and one this project cannot edit is reported to a different address entirely.
|
|
222
|
+
const isHeldCandidate = (finding) => finding.verdict === 'update' && !finding.isInherited;
|
|
223
|
+
const describeUpdates = async (reported, now) => await mapWithConcurrency(reported, REGISTRY_CONCURRENCY, async (finding) => {
|
|
224
|
+
if (!isHeldCandidate(finding)) {
|
|
225
|
+
return describeReported(finding);
|
|
226
|
+
}
|
|
227
|
+
const age = {
|
|
228
|
+
publishedAt: await publishedAt(finding.name, finding.latest),
|
|
229
|
+
now,
|
|
230
|
+
};
|
|
231
|
+
return isHeldByReleaseAge(age)
|
|
232
|
+
? describeHeld(finding, hoursPublished(age))
|
|
233
|
+
: describeReported(finding);
|
|
234
|
+
});
|
|
235
|
+
// One lookup per distinct name, fanned back out across the coordinates that share it. A workspace
|
|
236
|
+
// declaring the same analyzer in three manifests must not ask the registry three times.
|
|
237
|
+
//
|
|
238
|
+
// Windowed rather than all at once. `Promise.all` over the distinct names opened a socket per dependency
|
|
239
|
+
// - several hundred on a real workspace - which is what a public registry rate-limits and what a
|
|
240
|
+
// corporate proxy drops. Both arrive here as `unreachable`, so the gate blamed the network for a load
|
|
241
|
+
// the harness itself had generated.
|
|
242
|
+
const lookUpEach = async (names) => new Map(await mapWithConcurrency(names, REGISTRY_CONCURRENCY, async (name) => {
|
|
243
|
+
try {
|
|
244
|
+
return [name, await latestVersion(name)];
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return [name, { kind: 'unreachable' }];
|
|
248
|
+
}
|
|
249
|
+
}));
|
|
250
|
+
/**
|
|
251
|
+
* Fail when a declared dependency is two or more majors behind its latest release; report any lesser
|
|
252
|
+
* lag as an available update. This gate is fail-closed: it cannot prove freshness
|
|
253
|
+
* without the registry, so it fails rather than passing silently. A dependency the registry does not
|
|
254
|
+
* publish at all - a private or workspace-linked package - is reported rather than judged, because a
|
|
255
|
+
* public "latest" it has no claim to would be a fabricated comparison.
|
|
256
|
+
*/
|
|
257
|
+
// The images the harness pins are versioned inputs like the coordinates are, and they are read first so
|
|
258
|
+
// a registry that cannot answer fails before the longer npm fan-out runs. An image never fails for being
|
|
259
|
+
// behind; it fails only where the guideline says a freshness check does, which is when nothing can
|
|
260
|
+
// establish what is current.
|
|
261
|
+
const imagesUnprovable = (images) => images.failure === undefined
|
|
262
|
+
? undefined
|
|
263
|
+
: failed('image freshness cannot be proven', [
|
|
264
|
+
images.failure,
|
|
265
|
+
'this gate is fail-closed by design; retry with network access',
|
|
266
|
+
]);
|
|
267
|
+
// The summary may not claim every coordinate is within the bound while the report below it lists one
|
|
268
|
+
// that is not. A harness pin past the bound does not stop this build, and a pass that said nothing about
|
|
269
|
+
// it would leave the only line that matters to be found by reading. The images are counted here too:
|
|
270
|
+
// they are versioned inputs the same rule covers, so a summary naming only coordinates would understate
|
|
271
|
+
// what the gate read.
|
|
272
|
+
const freshnessSummary = (statuses, manifests, report, images) => {
|
|
273
|
+
const inherited = manifests.filter((manifest) => manifest.isInherited).length;
|
|
274
|
+
const overdue = report.reported.filter((finding) => finding.verdict === 'fail').length;
|
|
275
|
+
const counted = `${String(statuses.length)} declared coordinate(s) across ${String(manifests.length)} ` +
|
|
276
|
+
`manifest(s), ${String(inherited)} inherited, and ${String(images.scanned)} pinned image(s)`;
|
|
277
|
+
return overdue > 0
|
|
278
|
+
? `${counted}; ${String(overdue)} inherited coordinate(s) past the bound, reported not failed`
|
|
279
|
+
: `${counted}, are within the freshness bound`;
|
|
280
|
+
};
|
|
281
|
+
export const dependencyFreshness = async (context) => {
|
|
282
|
+
const images = await imageFreshness();
|
|
283
|
+
const unprovable = imagesUnprovable(images);
|
|
284
|
+
if (unprovable !== undefined) {
|
|
285
|
+
return unprovable;
|
|
286
|
+
}
|
|
287
|
+
const manifests = manifestSources(context);
|
|
288
|
+
const coordinates = collectCoordinates(manifests);
|
|
289
|
+
const names = [
|
|
290
|
+
...new Set(coordinates.map((coordinate) => coordinate.name)),
|
|
291
|
+
];
|
|
292
|
+
const { statuses, unreachable, unpublished } = sortLookups(coordinates, await lookUpEach(names));
|
|
293
|
+
if (unreachable.length > 0) {
|
|
294
|
+
return failed('the npm registry was unreachable, so freshness cannot be proven', [
|
|
295
|
+
`no "latest" resolved for: ${unreachable.slice(0, MAX_LISTED_NAMES).join(', ')}`,
|
|
296
|
+
// Stated so a reader can tell a registry that refused from one that never answered. Both land
|
|
297
|
+
// here, and only the second is worth investigating as a proxy or firewall rather than as an
|
|
298
|
+
// outage - which is unreadable from a message that says only "unreachable".
|
|
299
|
+
`each of ${String(REGISTRY_ATTEMPTS)} attempt(s) was given ${String(REQUEST_TIMEOUT_MS)}ms, so ` +
|
|
300
|
+
'a name listed above was either refused or never answered within that bound',
|
|
301
|
+
'this gate is fail-closed by design; retry with network access',
|
|
302
|
+
]);
|
|
303
|
+
}
|
|
304
|
+
const notes = unpublished.map((name) => `note ${name} is not on the public registry, so freshness is not measurable`);
|
|
305
|
+
const report = findFreshnessViolations(statuses);
|
|
306
|
+
if (report.failures.length > 0) {
|
|
307
|
+
return failed(`${String(report.failures.length)} dependency/dependencies are two or more majors behind`, [
|
|
308
|
+
...report.failures.map((finding) => describeFinding(finding)),
|
|
309
|
+
...(await describeUpdates(report.reported, Date.now())),
|
|
310
|
+
...images.lines,
|
|
311
|
+
...notes,
|
|
312
|
+
]);
|
|
313
|
+
}
|
|
314
|
+
return passed(freshnessSummary(statuses, manifests, report, images), [
|
|
315
|
+
...(await describeUpdates(report.reported, Date.now())),
|
|
316
|
+
...images.lines,
|
|
317
|
+
...notes,
|
|
318
|
+
]);
|
|
319
|
+
};
|
|
320
|
+
const asStrings = (raw) => isArray(raw) ? raw.filter((entry) => typeof entry === 'string') : [];
|
|
321
|
+
const textAt = (raw, key, fallback) => {
|
|
322
|
+
const found = asText(raw[key]);
|
|
323
|
+
return found.length > 0 ? found : fallback;
|
|
324
|
+
};
|
|
325
|
+
const asAdvisory = (key, raw) => ({
|
|
326
|
+
id: textAt(raw, 'github_advisory_id', key),
|
|
327
|
+
packageName: textAt(raw, 'module_name', 'unknown'),
|
|
328
|
+
// An advisory whose severity the record does not carry is read as the most severe, not the least.
|
|
329
|
+
// Defaulting to `info` put it below the threshold, so a malformed record was silently a pass inside a
|
|
330
|
+
// gate that is fail-closed everywhere else.
|
|
331
|
+
severity: textAt(raw, 'severity', 'unknown'),
|
|
332
|
+
title: textAt(raw, 'title', 'no title reported'),
|
|
333
|
+
aliases: asStrings(raw['cves']),
|
|
334
|
+
});
|
|
335
|
+
// `pnpm audit` exits non-zero whenever it finds anything, so the exit code carries no information the
|
|
336
|
+
// JSON does not. The verdict comes from the payload; only an unparseable payload means the database was
|
|
337
|
+
// unreachable, and that is a failure like any other - a check that cannot run is a fail.
|
|
338
|
+
const parseAudit = (output) => {
|
|
339
|
+
try {
|
|
340
|
+
const parsed = JSON.parse(output);
|
|
341
|
+
if (!(isRecord(parsed) && isRecord(parsed['metadata']))) {
|
|
342
|
+
return undefined;
|
|
343
|
+
}
|
|
344
|
+
return Object.entries(asRecord(parsed['advisories'])).map(([key, raw]) => asAdvisory(key, asRecord(raw)));
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
return undefined;
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
/**
|
|
351
|
+
* Read every package in the resolved set against the public advisory database.
|
|
352
|
+
* @param context the resolved project environment.
|
|
353
|
+
* @returns a failure for any finding at or above the declared severity, or for a stale exception.
|
|
354
|
+
*/
|
|
355
|
+
export const vulnerabilities = (context) => {
|
|
356
|
+
// No `--prod` filter: the standard is explicit that build and test packages count, because a build
|
|
357
|
+
// tool runs with build privileges and its vulnerability is reachable by anyone who can change the repo.
|
|
358
|
+
// Deadlined because it asks the advisory database over the network. Without one, a registry that
|
|
359
|
+
// accepts the connection and stalls holds the whole verification open with nothing printed.
|
|
360
|
+
const result = run('pnpm', ['audit', '--json'], {
|
|
361
|
+
cwd: context.root,
|
|
362
|
+
timeoutMs: NETWORKED_TOOL_TIMEOUT_MS,
|
|
363
|
+
});
|
|
364
|
+
// `stdout` for the reason the licence gate reads it: a pnpm warning on stderr used to be concatenated
|
|
365
|
+
// into the payload and reported as the advisory database being unreachable.
|
|
366
|
+
const advisories = parseAudit(result.stdout);
|
|
367
|
+
if (advisories === undefined) {
|
|
368
|
+
return failed('the advisory database was unreachable, so vulnerabilities cannot be proven', [
|
|
369
|
+
...(result.code === TIMED_OUT_CODE
|
|
370
|
+
? [`the audit did not answer within ${String(NETWORKED_TOOL_TIMEOUT_MS)}ms and was stopped`]
|
|
371
|
+
: []),
|
|
372
|
+
result.output.split('\n').slice(0, MAX_REPORTED_LINES).join('\n'),
|
|
373
|
+
'this gate is fail-closed by design; retry with network access',
|
|
374
|
+
]);
|
|
375
|
+
}
|
|
376
|
+
const report = judgeVulnerabilities(advisories, context.settings.vulnerabilityAllowlist, context.settings.vulnerabilitySeverity, HARNESS_EXCEPTIONS);
|
|
377
|
+
const findings = [
|
|
378
|
+
...report.unsuppressed.map((advisory) => `${advisory.severity} ${advisory.id} in ${advisory.packageName}: ${advisory.title}`),
|
|
379
|
+
...report.deadEntries,
|
|
380
|
+
];
|
|
381
|
+
// The evidence is the audit pnpm printed, kept whatever the verdict. A gate that reaches its verdict
|
|
382
|
+
// by PARSING a tool rather than by its exit status is exactly the one a reader wants to audit, and
|
|
383
|
+
// dropping the transcript here would leave `--verbose` answering "no output" on the gate that most
|
|
384
|
+
// needs an answer.
|
|
385
|
+
return withOutput(findings.length > 0
|
|
386
|
+
? failed(`${String(findings.length)} vulnerability finding(s) at or above ${report.threshold}`, findings)
|
|
387
|
+
: passed(`no vulnerability at or above ${report.threshold} across ${String(advisories.length)} advisory record(s)`), result.stdout);
|
|
388
|
+
};
|
|
389
|
+
//# sourceMappingURL=dependencies.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dependencies.js","sourceRoot":"","sources":["../../src/checks/dependencies.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type Context } from '../context.js';
|
|
2
|
+
import { type GateResult } from '../exec.js';
|
|
3
|
+
/** Check every tracked text file against the committed `.editorconfig`. */
|
|
4
|
+
export declare const editorconfig: (context: Context) => GateResult;
|
|
5
|
+
//# sourceMappingURL=editorconfig.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"editorconfig.d.ts","sourceRoot":"","sources":["../../src/checks/editorconfig.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,KAAK,OAAO,EAAgB,MAAM,eAAe,CAAA;AAC1D,OAAO,EAAU,KAAK,UAAU,EAAU,MAAM,YAAY,CAAA;AAS5D,2EAA2E;AAC3E,eAAO,MAAM,YAAY,GAAI,SAAS,OAAO,KAAG,UAoC/C,CAAA"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Conformance to the committed `.editorconfig`. The decision lives in governance; this reads the tree.
|
|
2
|
+
//
|
|
3
|
+
// Every tracked file is checked except those excluded by role: a binary asset, recognised from its own
|
|
4
|
+
// bytes, and a path the project declares generated. The standard's line cap applies to code roles only,
|
|
5
|
+
// because a line cap is a Code Rule and prose wraps by meaning rather than by column.
|
|
6
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { CODE_EXTENSIONS, findEditorconfigViolations, hasExtension, isBinary, matchesRole, parseEditorconfig, } from '@ploaness/governance';
|
|
9
|
+
import { trackedFiles } from '../context.js';
|
|
10
|
+
import { failed, passed } from '../exec.js';
|
|
11
|
+
const CONFIG_FILE = '.editorconfig';
|
|
12
|
+
/** Check every tracked text file against the committed `.editorconfig`. */
|
|
13
|
+
export const editorconfig = (context) => {
|
|
14
|
+
const configPath = path.join(context.root, CONFIG_FILE);
|
|
15
|
+
if (!existsSync(configPath)) {
|
|
16
|
+
return failed(`${CONFIG_FILE} is missing, so formatting cannot be proven`, [
|
|
17
|
+
`${CONFIG_FILE} is a managed file; restore it with \`ploaness sync\``,
|
|
18
|
+
]);
|
|
19
|
+
}
|
|
20
|
+
const rules = parseEditorconfig(readFileSync(configPath, 'utf8'));
|
|
21
|
+
// A tracked path is not always a regular file: a symlink and a submodule gitlink both appear here,
|
|
22
|
+
// and reading either throws rather than yielding text.
|
|
23
|
+
const tracked = trackedFiles(context.root).filter((file) => {
|
|
24
|
+
const full = path.join(context.root, file);
|
|
25
|
+
return existsSync(full) && statSync(full).isFile();
|
|
26
|
+
});
|
|
27
|
+
// Select first, then judge: the two steps read separately and neither accumulates into a mutable box.
|
|
28
|
+
const readable = tracked
|
|
29
|
+
.filter((file) => !matchesRole(file, context.settings.typographyExclusions))
|
|
30
|
+
.map((file) => ({ file, bytes: readFileSync(path.join(context.root, file)) }))
|
|
31
|
+
.filter((entry) => !isBinary(entry.bytes));
|
|
32
|
+
const findings = readable.flatMap((entry) => findEditorconfigViolations(entry.bytes.toString('utf8'), rules, hasExtension(entry.file, CODE_EXTENSIONS)).map((violation) => `${entry.file}:${String(violation.line)} ${violation.reason}`));
|
|
33
|
+
const checked = readable.length;
|
|
34
|
+
return findings.length > 0
|
|
35
|
+
? failed(`${String(findings.length)} formatting violation(s)`, findings)
|
|
36
|
+
: passed(`${String(checked)} tracked file(s) match ${CONFIG_FILE}`);
|
|
37
|
+
};
|
|
38
|
+
//# sourceMappingURL=editorconfig.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"editorconfig.js","sourceRoot":"","sources":["../../src/checks/editorconfig.ts"],"names":[],"mappings":"AAAA,uGAAuG;AACvG,EAAE;AACF,uGAAuG;AACvG,wGAAwG;AACxG,sFAAsF;AACtF,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAC5D,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EACL,eAAe,EAGf,0BAA0B,EAC1B,YAAY,EACZ,QAAQ,EACR,WAAW,EACX,iBAAiB,GAClB,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAgB,YAAY,EAAE,MAAM,eAAe,CAAA;AAC1D,OAAO,EAAE,MAAM,EAAmB,MAAM,EAAE,MAAM,YAAY,CAAA;AAE5D,MAAM,WAAW,GAAW,eAAe,CAAA;AAO3C,2EAA2E;AAC3E,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,OAAgB,EAAc,EAAE;IAC3D,MAAM,UAAU,GAAW,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,WAAW,CAAC,CAAA;IAC/D,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC5B,OAAO,MAAM,CAAC,GAAG,WAAW,6CAA6C,EAAE;YACzE,GAAG,WAAW,uDAAuD;SACtE,CAAC,CAAA;IACJ,CAAC;IACD,MAAM,KAAK,GAAsB,iBAAiB,CAAC,YAAY,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC,CAAA;IACpF,mGAAmG;IACnG,uDAAuD;IACvD,MAAM,OAAO,GAAsB,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,IAAY,EAAW,EAAE;QAC7F,MAAM,IAAI,GAAW,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;QAClD,OAAO,UAAU,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,CAAA;IACpD,CAAC,CAAC,CAAA;IAEF,sGAAsG;IACtG,MAAM,QAAQ,GAAwB,OAAO;SAC1C,MAAM,CAAC,CAAC,IAAY,EAAW,EAAE,CAAC,CAAC,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAC,CAAC;SAC5F,GAAG,CAAC,CAAC,IAAY,EAAY,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC;SAC/F,MAAM,CAAC,CAAC,KAAe,EAAW,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAA;IAE/D,MAAM,QAAQ,GAAsB,QAAQ,CAAC,OAAO,CAAC,CAAC,KAAe,EAAqB,EAAE,CAC1F,0BAA0B,CACxB,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,EAC5B,KAAK,EACL,YAAY,CAAC,KAAK,CAAC,IAAI,EAAE,eAAe,CAAC,CAC1C,CAAC,GAAG,CACH,CAAC,SAAgC,EAAU,EAAE,CAC3C,GAAG,KAAK,CAAC,IAAI,IAAI,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,SAAS,CAAC,MAAM,EAAE,CAChE,CACF,CAAA;IACD,MAAM,OAAO,GAAW,QAAQ,CAAC,MAAM,CAAA;IAEvC,OAAO,QAAQ,CAAC,MAAM,GAAG,CAAC;QACxB,CAAC,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,0BAA0B,EAAE,QAAQ,CAAC;QACxE,CAAC,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,0BAA0B,WAAW,EAAE,CAAC,CAAA;AACvE,CAAC,CAAA"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Repository } from '../context.js';
|
|
2
|
+
import { type GateResult } from '../exec.js';
|
|
3
|
+
export declare const generatedDenial: (context: Repository) => GateResult;
|
|
4
|
+
/**
|
|
5
|
+
* Merge the required deny rules into the project's runtime settings.
|
|
6
|
+
* @param context the resolved project environment.
|
|
7
|
+
* @returns true when the file was created or changed.
|
|
8
|
+
*/
|
|
9
|
+
export declare const hasWrittenDenyRules: (context: Repository) => boolean;
|
|
10
|
+
//# sourceMappingURL=generated.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generated.d.ts","sourceRoot":"","sources":["../../src/checks/generated.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAU,UAAU,EAAE,MAAM,eAAe,CAAA;AACvD,OAAO,EAAU,KAAK,UAAU,EAAU,MAAM,YAAY,CAAA;AAyC5D,eAAO,MAAM,eAAe,GAAI,SAAS,UAAU,KAAG,UAerD,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAAI,SAAS,UAAU,KAAG,OAiBzD,CAAA"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Write denial for the generated Payload artefacts. The decision lives in governance; this reads the
|
|
2
|
+
// runtime settings file and, on the sync path, writes it back.
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { applyDenyRules, deniedPathsFor, findDenialViolations, parseJsonc, } from '@ploaness/governance';
|
|
6
|
+
import { failed, passed } from '../exec.js';
|
|
7
|
+
// Matches the indent the shipped formatter settings declare.
|
|
8
|
+
const JSON_INDENT = 2;
|
|
9
|
+
const SETTINGS_PATH = '.claude/settings.json';
|
|
10
|
+
const LOCAL_SETTINGS_PATH = '.claude/settings.local.json';
|
|
11
|
+
// Absent and unparseable used to collapse into the same `undefined`, and `hasWrittenDenyRules` then
|
|
12
|
+
// merged its rules into that nothing and wrote the result. One stray comma in a project's settings and
|
|
13
|
+
// `ploaness sync` replaced every permission, hook, and environment entry the file held with the deny
|
|
14
|
+
// list alone. A tool may refuse to write; it may not quietly discard what it could not read.
|
|
15
|
+
const readSettings = (root, relative) => {
|
|
16
|
+
const full = path.join(root, relative);
|
|
17
|
+
if (!existsSync(full)) {
|
|
18
|
+
return { value: undefined, isMalformed: false };
|
|
19
|
+
}
|
|
20
|
+
const read = parseJsonc(readFileSync(full, 'utf8'));
|
|
21
|
+
return { value: read.value, isMalformed: read.problem !== undefined };
|
|
22
|
+
};
|
|
23
|
+
const readJson = (root, relative) => readSettings(root, relative).value;
|
|
24
|
+
/** Check that the agent runtime is denied write access to every generated artefact. */
|
|
25
|
+
// The deny list is the union over Payload members of `<member>/<artefact>`. The runtime reads one
|
|
26
|
+
// settings file, at the repository root, while the artefacts are member-relative - so a rule written
|
|
27
|
+
// for one member would bind nothing in a workspace, and a gate per member would have several of them
|
|
28
|
+
// fighting over one file. For a single member at the root the union is the list that always shipped.
|
|
29
|
+
const deniedArtefacts = (repository) => repository.members
|
|
30
|
+
.filter((member) => member.isPayload)
|
|
31
|
+
.flatMap((member) => deniedPathsFor([member.path], member.settings.generatedArtefacts));
|
|
32
|
+
export const generatedDenial = (context) => {
|
|
33
|
+
const findings = findDenialViolations(readJson(context.root, SETTINGS_PATH), readJson(context.root, LOCAL_SETTINGS_PATH), deniedArtefacts(context));
|
|
34
|
+
return findings.length > 0
|
|
35
|
+
? failed(`${String(findings.length)} generated file(s) are writable by an agent`, [
|
|
36
|
+
...findings,
|
|
37
|
+
// Said plainly rather than implied: the rule binds the runtime that reads this file. The
|
|
38
|
+
// standard scopes it the same way, so this is the whole of what the rule claims.
|
|
39
|
+
`a denial in ${SETTINGS_PATH} binds the runtime that reads it; the regeneration gate ` +
|
|
40
|
+
'remains the check that catches an edit made another way',
|
|
41
|
+
])
|
|
42
|
+
: passed('every generated artefact is denied to an agent');
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Merge the required deny rules into the project's runtime settings.
|
|
46
|
+
* @param context the resolved project environment.
|
|
47
|
+
* @returns true when the file was created or changed.
|
|
48
|
+
*/
|
|
49
|
+
export const hasWrittenDenyRules = (context) => {
|
|
50
|
+
const full = path.join(context.root, SETTINGS_PATH);
|
|
51
|
+
const existing = readSettings(context.root, SETTINGS_PATH);
|
|
52
|
+
if (existing.isMalformed) {
|
|
53
|
+
throw new Error(`${SETTINGS_PATH} exists but is not valid JSON; repair it before running \`ploaness sync\`, ` +
|
|
54
|
+
'because merging into a file that could not be read would discard everything it holds');
|
|
55
|
+
}
|
|
56
|
+
const merged = applyDenyRules(existing.value, deniedArtefacts(context));
|
|
57
|
+
const text = `${JSON.stringify(merged, null, JSON_INDENT)}\n`;
|
|
58
|
+
if (existsSync(full) && readFileSync(full, 'utf8') === text) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
mkdirSync(path.dirname(full), { recursive: true });
|
|
62
|
+
writeFileSync(full, text);
|
|
63
|
+
return true;
|
|
64
|
+
};
|
|
65
|
+
//# sourceMappingURL=generated.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generated.js","sourceRoot":"","sources":["../../src/checks/generated.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type Context } from '../context.js';
|
|
2
|
+
import { type GateResult } from '../exec.js';
|
|
3
|
+
export declare const requireFullHistory: (context: Context) => GateResult;
|
|
4
|
+
/**
|
|
5
|
+
* Validate commit messages. Every commit is held to every rule, including the ones git writes for you: a
|
|
6
|
+
* merge, a revert, or an autosquash subject must be given a conforming form by hand.
|
|
7
|
+
*
|
|
8
|
+
* It walks the same refs `linearHistory` does, and the default is the shared constant rather than a
|
|
9
|
+
* literal so the two cannot drift apart. Walking only HEAD was the earlier spelling and it left a hole
|
|
10
|
+
* this repository's own rules describe: a branch off HEAD's ancestry is history the repository owns and
|
|
11
|
+
* can rewrite, so a banned message there is a violation nothing reported. A real project carried an
|
|
12
|
+
* agent-attribution trailer on such a branch past a passing gate.
|
|
13
|
+
*/
|
|
14
|
+
export declare const commitHistory: (context: Context, revisionArguments?: readonly string[]) => GateResult;
|
|
15
|
+
/** Validate one pending message, from a message file the author points at. */
|
|
16
|
+
export declare const commitMessageProblems: (context: Context, raw: string) => readonly string[];
|
|
17
|
+
/** The history is linear: a merge commit is prohibited wherever this repository owns it. */
|
|
18
|
+
export declare const linearHistory: (context: Context) => GateResult;
|
|
19
|
+
//# sourceMappingURL=history.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"history.d.ts","sourceRoot":"","sources":["../../src/checks/history.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,KAAK,OAAO,EAAO,MAAM,eAAe,CAAA;AACjD,OAAO,EAAU,KAAK,UAAU,EAAU,MAAM,YAAY,CAAA;AAY5D,eAAO,MAAM,kBAAkB,GAAI,SAAS,OAAO,KAAG,UAUrD,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,aAAa,GACxB,SAAS,OAAO,EAChB,oBAAmB,SAAS,MAAM,EAA4B,KAC7D,UAmBF,CAAA;AAED,8EAA8E;AAC9E,eAAO,MAAM,qBAAqB,GAAI,SAAS,OAAO,EAAE,KAAK,MAAM,KAAG,SAAS,MAAM,EAKpF,CAAA;AAOD,4FAA4F;AAC5F,eAAO,MAAM,aAAa,GAAI,SAAS,OAAO,KAAG,UA6BhD,CAAA"}
|