simt-dev 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -0
- package/README.md +129 -75
- package/docs/decisions/ADR-073-a-kernel-is-a-smaller-projection-the-repository-asks-for.md +1 -1
- package/docs/decisions/ADR-077-the-command-card-leads-the-region-and-reads-committed-evidence.md +1 -1
- package/docs/decisions/ADR-083-a-budget-is-met-by-cutting-the-kernel-and-hard-is-the-floor.md +153 -0
- package/docs/decisions/ADR-084-a-workspace-carries-its-repeated-rules-once.md +100 -0
- package/docs/decisions/ADR-085-the-region-ends-with-the-names-it-kept-out.md +72 -0
- package/docs/decisions/ADR-086-the-stable-prefix-comes-first-and-the-lock-records-both-digests.md +69 -0
- package/docs/decisions/ADR-087-a-run-is-evidence-for-one-tree.md +67 -0
- package/docs/decisions/ADR-088-a-marked-line-is-enforced-by-a-checker.md +59 -0
- package/docs/decisions/ADR-089-the-local-index-is-a-cache-of-git-blobs.md +52 -0
- package/docs/decisions/ADR-090-an-optional-adapter-is-off-until-named.md +55 -0
- package/docs/decisions/ADR-091-the-map-ranks-files-a-task-mentions.md +60 -0
- package/docs/decisions/ADR-092-tests-are-imports-conventions-and-coverage.md +60 -0
- package/docs/decisions/ADR-093-why-names-what-governs-a-file.md +58 -0
- package/docs/decisions/ADR-094-a-change-runs-the-tests-that-import-it.md +44 -0
- package/docs/decisions/ADR-095-gate-output-keeps-the-failures.md +31 -0
- package/docs/decisions/ADR-096-impact-names-who-a-change-reaches.md +38 -0
- package/package.json +1 -1
- package/rules/core.md +1 -1
- package/src/argv.mjs +18 -0
- package/src/ci/workflow.mjs +11 -1
- package/src/commands/bench.mjs +13 -1
- package/src/commands/evidence.mjs +96 -0
- package/src/commands/gate.mjs +165 -0
- package/src/commands/help.mjs +31 -0
- package/src/commands/hooks.mjs +45 -5
- package/src/commands/impact.mjs +205 -0
- package/src/commands/index.mjs +35 -0
- package/src/commands/map.mjs +91 -0
- package/src/commands/repoindex.mjs +52 -0
- package/src/commands/shared.mjs +398 -0
- package/src/commands/sync.mjs +72 -3
- package/src/commands/tests.mjs +192 -0
- package/src/commands/tokens.mjs +30 -1
- package/src/commands/why.mjs +177 -0
- package/src/decisions/index.mjs +1 -1
- package/src/graph/spec.mjs +1 -1
- package/src/hooks/dispatcher.mjs +15 -0
- package/src/hooks/shim.mjs +31 -0
- package/src/index/compact.mjs +228 -0
- package/src/index/extract.mjs +331 -0
- package/src/index/gate.mjs +101 -0
- package/src/index/govern.mjs +222 -0
- package/src/index/impact.mjs +186 -0
- package/src/index/map.mjs +393 -0
- package/src/index/persist.mjs +172 -0
- package/src/index/store.mjs +301 -0
- package/src/index/tests.mjs +294 -0
- package/src/kernel/budget.mjs +100 -0
- package/src/kernel/checks.mjs +378 -0
- package/src/ledger/index.mjs +112 -0
- package/src/measure/context.mjs +7 -5
- package/src/projection/disclosure.mjs +92 -0
- package/src/projection/lock.mjs +93 -0
- package/src/projection/manifest.mjs +52 -0
- package/src/projection/markers.mjs +56 -17
- package/src/projection/project.mjs +24 -17
- package/src/projection/rules.mjs +378 -44
- package/src/projection/shared.mjs +428 -0
- package/src/projection/targets.mjs +2 -1
- package/src/toolwords.mjs +2 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// The lock records the two digests a frozen check compares (ADR-086).
|
|
2
|
+
//
|
|
3
|
+
// Input is everything the projection reads that a repository can change, plus the
|
|
4
|
+
// digest of what this package ships. Output is the region body. An output that moves
|
|
5
|
+
// while the input does not is a renderer that is not a function of its inputs, and
|
|
6
|
+
// that is the failure `--frozen` exists to catch. An input that moves is a stale
|
|
7
|
+
// lock: the projection changed because the repository did, and the lock was not
|
|
8
|
+
// rewritten.
|
|
9
|
+
|
|
10
|
+
import { createHash } from 'node:crypto';
|
|
11
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
|
|
14
|
+
import { fingerprint } from '../resolve/fingerprint.mjs';
|
|
15
|
+
import { desiredRegion, provenanceDigest, LOCAL_PROFILE_DIR } from './rules.mjs';
|
|
16
|
+
import { readManifest } from './manifest.mjs';
|
|
17
|
+
|
|
18
|
+
export const LOCK_FILE = 'simt-dev.lock.json';
|
|
19
|
+
|
|
20
|
+
/** Manifest keys that change what the region contains. Anything else in the file is
|
|
21
|
+
* the repository's own notes, and hashing it would make a comment fail the lock. */
|
|
22
|
+
const MANIFEST_KEYS = ['mode', 'budget', 'shared', 'surfaces', 'gates'];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The lock this repository's projection should have.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} root
|
|
28
|
+
* @param {object} [flags] the same flags `sync` rendered with, so the output is the
|
|
29
|
+
* body that was written and not a body for a different set of surfaces
|
|
30
|
+
*/
|
|
31
|
+
export async function computeLock(root, flags = {}) {
|
|
32
|
+
const desired = await desiredRegion(root, flags);
|
|
33
|
+
return {
|
|
34
|
+
inputs: await inputDigest(root),
|
|
35
|
+
outputs: createHash('sha256').update(desired.body).digest('hex'),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Read a lock, or null when the file is not there. A file that does not parse is an
|
|
40
|
+
* error the caller reports, not a missing lock: absence and damage are different
|
|
41
|
+
* repairs. */
|
|
42
|
+
export async function readLock(root) {
|
|
43
|
+
let text;
|
|
44
|
+
try {
|
|
45
|
+
text = await readFile(join(root, LOCK_FILE), 'utf8');
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (error.code === 'ENOENT') return { lock: null, error: null };
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const lock = JSON.parse(text);
|
|
52
|
+
if (typeof lock?.inputs !== 'string' || typeof lock?.outputs !== 'string') {
|
|
53
|
+
return { lock: null, error: 'must be an object with inputs and outputs' };
|
|
54
|
+
}
|
|
55
|
+
return { lock, error: null };
|
|
56
|
+
} catch (error) {
|
|
57
|
+
return { lock: null, error: error.message };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function inputDigest(root) {
|
|
62
|
+
const hash = createHash('sha256');
|
|
63
|
+
hash.update(`provenance:${await provenanceDigest()}\n`);
|
|
64
|
+
const { stack, facts } = await fingerprint(root);
|
|
65
|
+
hash.update(`signals:${JSON.stringify({ stack, facts })}\n`);
|
|
66
|
+
const { manifest } = await readManifest(root);
|
|
67
|
+
const owned = {};
|
|
68
|
+
if (manifest && typeof manifest === 'object') {
|
|
69
|
+
for (const key of MANIFEST_KEYS) {
|
|
70
|
+
if (manifest[key] !== undefined) owned[key] = manifest[key];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
hash.update(`manifest:${JSON.stringify(owned)}\n`);
|
|
74
|
+
let names = [];
|
|
75
|
+
try {
|
|
76
|
+
names = (await readdir(join(root, LOCAL_PROFILE_DIR))).filter((n) => n.endsWith('.md')).sort();
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (error.code !== 'ENOENT') throw error;
|
|
79
|
+
}
|
|
80
|
+
for (const name of names) {
|
|
81
|
+
const bytes = await readFile(join(root, LOCAL_PROFILE_DIR, name));
|
|
82
|
+
hash.update(`profile:${name}:${bytes.length}:`);
|
|
83
|
+
hash.update(bytes);
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const raw = await readFile(join(root, 'package.json'), 'utf8');
|
|
87
|
+
const scripts = JSON.parse(raw).scripts ?? {};
|
|
88
|
+
hash.update(`scripts:${JSON.stringify(scripts)}\n`);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (error.code !== 'ENOENT' && !(error instanceof SyntaxError)) throw error;
|
|
91
|
+
}
|
|
92
|
+
return hash.digest('hex');
|
|
93
|
+
}
|
|
@@ -463,6 +463,15 @@ export function buildManifest({ root, stack = [], sha, version, existing = null,
|
|
|
463
463
|
// compliance (plan 0003 R02) — a mode chosen by default would be a bet the
|
|
464
464
|
// repository never made. Declared, not generated, for the same reason as `budget`.
|
|
465
465
|
...(existing?.mode === undefined ? {} : { mode: existing.mode }),
|
|
466
|
+
// ADR-084: the rules this repository hands to the workspace's user-level copy, and
|
|
467
|
+
// the level that copy lives at. Declared, not generated, for the same reason as
|
|
468
|
+
// `budget` and `mode`: `shared install` writes the list once, but the bytes that land
|
|
469
|
+
// in a repository's region depend on it on every `sync` — so if `upgrade` rewrote it
|
|
470
|
+
// from its own opinion of what a workspace shares, a repository's region would change
|
|
471
|
+
// without anybody asking, which is the failure `budget` moving by itself was.
|
|
472
|
+
// Absent means "no repository in this workspace shares anything", which is the state
|
|
473
|
+
// every repository is in until somebody runs `shared install`.
|
|
474
|
+
...(existing?.shared === undefined ? {} : { shared: existing.shared }),
|
|
466
475
|
// ADR-020: which editors this repo's rules are projected into. Absent means the
|
|
467
476
|
// global standard applies; an explicit empty list is a real choice meaning
|
|
468
477
|
// AGENTS.md only. Recorded rather than inferred, because "which IDE does this repo
|
|
@@ -631,6 +640,24 @@ function defaultId(root) {
|
|
|
631
640
|
*/
|
|
632
641
|
export const DEFAULT_BUDGET_BYTES = 30000;
|
|
633
642
|
|
|
643
|
+
/**
|
|
644
|
+
* The budget a repository declared, or the default it gets for declaring nothing.
|
|
645
|
+
*
|
|
646
|
+
* One reader for one number, and it exists because two callers need it and a third
|
|
647
|
+
* already had its own: the packer that cuts a kernel to fit (ADR-083) and the meter that
|
|
648
|
+
* reports what it carries, against a ceiling each would otherwise spell differently. The
|
|
649
|
+
* byte they disagree about is the byte the repository wrote down.
|
|
650
|
+
*
|
|
651
|
+
* `0` is honoured — "this repository wants an empty projection" is a decision somebody can
|
|
652
|
+
* make. `null`, a string and `NaN` are not budgets at all, and a hand-edited manifest is
|
|
653
|
+
* exactly where one turns up; the default stands in rather than the ceiling becoming a
|
|
654
|
+
* string that every comparison against it silently passes.
|
|
655
|
+
*/
|
|
656
|
+
export function declaredBudget(manifest) {
|
|
657
|
+
const declared = manifest?.budget?.bytes;
|
|
658
|
+
return Number.isFinite(declared) && declared >= 0 ? declared : DEFAULT_BUDGET_BYTES;
|
|
659
|
+
}
|
|
660
|
+
|
|
634
661
|
/**
|
|
635
662
|
* Report a projection that carries more than the repository agreed to carry.
|
|
636
663
|
*
|
|
@@ -667,6 +694,31 @@ export function budgetFinding(bytes, budget, mode = 'full') {
|
|
|
667
694
|
};
|
|
668
695
|
}
|
|
669
696
|
|
|
697
|
+
/**
|
|
698
|
+
* The workspace-shared rules this repository declared, as a set of ids.
|
|
699
|
+
*
|
|
700
|
+
* One reader, because the region, `doctor` and `shared` all need the same answer and a
|
|
701
|
+
* projection that renders a delta the meter cannot see is the one-question-two-answers
|
|
702
|
+
* defect this repository exists to end.
|
|
703
|
+
*
|
|
704
|
+
* `level` is read and validated rather than assumed, because `user` is the only level any
|
|
705
|
+
* reader loads from outside a repository and a hand-edited manifest is exactly where a
|
|
706
|
+
* second value turns up. A list that is not a list of strings yields an empty set: the
|
|
707
|
+
* repository then projects everything it selects, which is the state it was in before
|
|
708
|
+
* anybody ran `shared install`, and is never a state that loses a rule.
|
|
709
|
+
*/
|
|
710
|
+
export function declaredShared(manifest) {
|
|
711
|
+
const shared = manifest?.shared;
|
|
712
|
+
if (shared === undefined || shared === null) return { level: null, rules: new Set() };
|
|
713
|
+
const level = shared.level === 'user' ? 'user' : null;
|
|
714
|
+
const rules = new Set(Array.isArray(shared.rules) ? shared.rules.filter((id) => typeof id === 'string') : []);
|
|
715
|
+
// A level of `user` with nothing in it is a declaration that changes nothing, and a
|
|
716
|
+
// level that is not `user` names a place this tool cannot write — so neither is a
|
|
717
|
+
// reason to drop rules out of a region.
|
|
718
|
+
if (level === null || rules.size === 0) return { level: null, rules: new Set() };
|
|
719
|
+
return { level, rules };
|
|
720
|
+
}
|
|
721
|
+
|
|
670
722
|
export const GENERATED_KEYS = new Set(['$comment', 'projection']);
|
|
671
723
|
|
|
672
724
|
/**
|
|
@@ -189,6 +189,57 @@ function readSha(body, style) {
|
|
|
189
189
|
* never drift independently of it.
|
|
190
190
|
*/
|
|
191
191
|
export function compose(body, { sha, sources = [] } = {}, style) {
|
|
192
|
+
return render(body, { sha, sources }, style, (s, sha, sources) =>
|
|
193
|
+
[
|
|
194
|
+
s.line(`${MARKER_PREFIX}:generated`),
|
|
195
|
+
s.line(`${MARKER_PREFIX}:sha=${sha}`),
|
|
196
|
+
...(sources.length > 0 ? [s.line(`${MARKER_PREFIX}:sources=${sources.join(',')}`)] : []),
|
|
197
|
+
// Addressed to whoever reads this repo, not to whoever owns the rules upstream.
|
|
198
|
+
// "Edit the source in simt-dev" was the wrong instruction twice over: a consumer
|
|
199
|
+
// of the published package has no such tree, and an agent reading this repo has
|
|
200
|
+
// no business editing the region it was told not to edit. The only two actions
|
|
201
|
+
// available to a reader are: don't edit it, or regenerate it.
|
|
202
|
+
s.note(
|
|
203
|
+
'Generated content. Do not edit by hand - your changes are overwritten.',
|
|
204
|
+
`Run \`${CLI} sync --yes\` to regenerate, or \`${CLI} check\` to see what is stale.`,
|
|
205
|
+
),
|
|
206
|
+
],
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* The user-level shared region (ADR-084).
|
|
212
|
+
*
|
|
213
|
+
* A different note from `compose`, and the difference is the whole point of having two
|
|
214
|
+
* functions. `compose`'s remedy is `simt sync --yes`, and that remedy is wrong for a file
|
|
215
|
+
* in the reader's home directory: `sync` acts on a repository, so a reader who follows
|
|
216
|
+
* the instruction inside `~/.config/opencode/AGENTS.md` runs a command that will not
|
|
217
|
+
* repair it and concludes the projection is broken. The stamp and the marker grammar are
|
|
218
|
+
* shared, so `parse`, `inject` and `diff` read this region exactly as they read a
|
|
219
|
+
* repository's own — only the two sentences a reader acts on differ.
|
|
220
|
+
*/
|
|
221
|
+
export function composeShared(body, { sha, sources = [] } = {}, style) {
|
|
222
|
+
return render(body, { sha, sources }, style, (s, sha, sources) =>
|
|
223
|
+
[
|
|
224
|
+
s.line(`${MARKER_PREFIX}:generated`),
|
|
225
|
+
s.line(`${MARKER_PREFIX}:sha=${sha}`),
|
|
226
|
+
...(sources.length > 0 ? [s.line(`${MARKER_PREFIX}:sources=${sources.join(',')}`)] : []),
|
|
227
|
+
s.note(
|
|
228
|
+
'Generated content shared by every repository in this workspace. Do not edit it by hand - your changes are overwritten.',
|
|
229
|
+
`Run \`${CLI} shared install --yes\` to regenerate it, or \`${CLI} doctor\` to see whether this copy is stale.`,
|
|
230
|
+
),
|
|
231
|
+
],
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* A region, markers and provenance header included, with a header this module chose.
|
|
237
|
+
*
|
|
238
|
+
* Split out because two headers now exist and the trailing-newline and empty-body cases
|
|
239
|
+
* below are load-bearing twice over — the formatter fight the header shape prevents does
|
|
240
|
+
* not care which of the two files it is happening in.
|
|
241
|
+
*/
|
|
242
|
+
function render(body, { sha, sources = [] }, style, headerLines) {
|
|
192
243
|
const s = styleOf(style);
|
|
193
244
|
// A stamp this tool cannot read back is worse than no stamp: `parse` matches
|
|
194
245
|
// `[0-9a-f]{7,40}`, so the old `unknown` fallback produced a region that reported
|
|
@@ -200,21 +251,7 @@ export function compose(body, { sha, sources = [] } = {}, style) {
|
|
|
200
251
|
}
|
|
201
252
|
// Write-new, like the markers: the legacy spelling is read but never emitted, so a
|
|
202
253
|
// migrated region is stable and a legacy one drifts exactly once.
|
|
203
|
-
const header =
|
|
204
|
-
if (sources.length > 0) {
|
|
205
|
-
header.push(s.line(`${MARKER_PREFIX}:sources=${sources.join(',')}`));
|
|
206
|
-
}
|
|
207
|
-
// Addressed to whoever reads this repo, not to whoever owns the rules upstream.
|
|
208
|
-
// "Edit the source in simt-dev" was the wrong instruction twice over: a consumer
|
|
209
|
-
// of the published package has no such tree, and an agent reading this repo has
|
|
210
|
-
// no business editing the region it was told not to edit. The only two actions
|
|
211
|
-
// available to a reader are: don't edit it, or regenerate it.
|
|
212
|
-
header.push(
|
|
213
|
-
s.note(
|
|
214
|
-
'Generated content. Do not edit by hand - your changes are overwritten.',
|
|
215
|
-
`Run \`${CLI} sync --yes\` to regenerate, or \`${CLI} check\` to see what is stale.`,
|
|
216
|
-
),
|
|
217
|
-
);
|
|
254
|
+
const header = headerLines(s, sha, sources);
|
|
218
255
|
const trimmed = body.replace(/\n+$/, '');
|
|
219
256
|
// Two lines here exist only because a real repo proved they are needed. A
|
|
220
257
|
// formatter that runs on commit - several repositories have one, and three of the
|
|
@@ -269,7 +306,9 @@ function splice(state, region, where) {
|
|
|
269
306
|
* adopted?" — `region: 'absent'` is that answer, and reading it off `previousBytes` gets a
|
|
270
307
|
* repository with hand-written prose and no region wrong.
|
|
271
308
|
*
|
|
272
|
-
* @param {object} [opts] `style` overrides the syntax; `where` overrides placement
|
|
309
|
+
* @param {object} [opts] `style` overrides the syntax; `where` overrides placement;
|
|
310
|
+
* `compose` overrides which header the region carries (`composeShared` for the
|
|
311
|
+
* user-level file, whose remedy is not `sync`).
|
|
273
312
|
* @returns {{text: string, action: 'added'|'updated'|'unchanged'|'malformed', reason?: string,
|
|
274
313
|
* region: 'absent'|'ok'|'malformed', previousBody: string}}
|
|
275
314
|
*/
|
|
@@ -281,7 +320,7 @@ export function inject(text, body, meta, opts = {}) {
|
|
|
281
320
|
if (state.status === 'malformed') {
|
|
282
321
|
return { ...seen, text, action: 'malformed', reason: state.reason, line: state.line };
|
|
283
322
|
}
|
|
284
|
-
const region = compose(body, meta, s);
|
|
323
|
+
const region = (opts.compose ?? compose)(body, meta, s);
|
|
285
324
|
|
|
286
325
|
if (state.status === 'absent') {
|
|
287
326
|
if (where === 'top') {
|
|
@@ -26,6 +26,7 @@ import { checkTsconfig } from '../tsconfig.mjs';
|
|
|
26
26
|
import { checkToolchain } from '../toolchain.mjs';
|
|
27
27
|
import { diff, parse, inject, BEGIN, END } from './markers.mjs';
|
|
28
28
|
import { budgetFinding, readManifest } from './manifest.mjs';
|
|
29
|
+
import { budgetCutFinding } from '../kernel/budget.mjs';
|
|
29
30
|
import { measureRoot } from '../measure/context.mjs';
|
|
30
31
|
import { loadMdcRules, renderMdc } from './mdc.mjs';
|
|
31
32
|
import { foldsRules, resolveSurfaces, SURFACES, SURFACE_IDS } from './surfaces.mjs';
|
|
@@ -165,23 +166,6 @@ export async function checkTargets(root, flags = {}) {
|
|
|
165
166
|
findings.push(...readLimitFindings(text, PRIMARY_TARGET));
|
|
166
167
|
}
|
|
167
168
|
|
|
168
|
-
// The budget: what this repository agreed to carry, against what it carries.
|
|
169
|
-
//
|
|
170
|
-
// Measured from the same `desiredRegion` the region above was compared against, plus
|
|
171
|
-
// the rule files this function went on to compare, because a gate and a meter that
|
|
172
|
-
// each counted their own set would report two different totals for one repository and
|
|
173
|
-
// the reader would have no way to tell which was the budget. Both numbers are the
|
|
174
|
-
// DESIRED ones, so a drifted repo is reported against what it would carry after a
|
|
175
|
-
// sync rather than what it carries today.
|
|
176
|
-
//
|
|
177
|
-
// Non-blocking by design: nothing is cut, because there is nothing to cut yet (ADR-072).
|
|
178
|
-
const budgetTotal = await alwaysOnBytes(root);
|
|
179
|
-
if (budgetTotal !== null) {
|
|
180
|
-
const { manifest } = await readManifest(root);
|
|
181
|
-
const over = budgetFinding(budgetTotal.bytes, manifest?.budget, manifest?.mode);
|
|
182
|
-
if (over) findings.push({ ...over, file: MANIFEST_FILE, detail: over.detail });
|
|
183
|
-
}
|
|
184
|
-
|
|
185
169
|
// Every package of a monorepo is a projection root in its own right, so the gate
|
|
186
170
|
// covers it the way it covers the root. A nested region that drifts is as stale as
|
|
187
171
|
// the root's region, and the reader working in that package reads only that file.
|
|
@@ -246,6 +230,29 @@ export async function checkTargets(root, flags = {}) {
|
|
|
246
230
|
const all = await loadMdcRules();
|
|
247
231
|
const desired = await desiredRegion(root, flags);
|
|
248
232
|
const { stack, facts, manifest } = desired;
|
|
233
|
+
|
|
234
|
+
// The budget, both halves of it, and in that order: what this repository agreed to
|
|
235
|
+
// carry against what it carries, then what was cut to get there.
|
|
236
|
+
//
|
|
237
|
+
// The total comes from the meter rather than from a sum this file could do itself,
|
|
238
|
+
// because a gate and a meter that each counted their own set would report two different
|
|
239
|
+
// numbers for one repository and the reader would have no way to tell which was the
|
|
240
|
+
// budget (ADR-072). The cut comes from the projection itself for the same reason — the
|
|
241
|
+
// packer wrote it, so the packer's own record of it is the one that cannot disagree with
|
|
242
|
+
// what `sync` would write.
|
|
243
|
+
//
|
|
244
|
+
// Both are non-blocking, for the reason ADR-072 gave for the first and ADR-083 for the
|
|
245
|
+
// second: the remedy is a decision about the number, not a missing file.
|
|
246
|
+
const budgetTotal = await alwaysOnBytes(root);
|
|
247
|
+
if (budgetTotal !== null) {
|
|
248
|
+
const over = budgetFinding(budgetTotal.bytes, manifest?.budget, manifest?.mode);
|
|
249
|
+
if (over) findings.push({ ...over, file: MANIFEST_FILE, detail: over.detail });
|
|
250
|
+
}
|
|
251
|
+
const packed = desired.budget;
|
|
252
|
+
if (packed !== null && packed.cuts.length > 0) {
|
|
253
|
+
findings.push(budgetCutFinding({ file: PRIMARY_TARGET, ...packed }));
|
|
254
|
+
}
|
|
255
|
+
|
|
249
256
|
const declared = await readDeclared(root);
|
|
250
257
|
const resolved = resolveSurfaces(manifest, flags);
|
|
251
258
|
for (const p of resolved.problems) {
|