@variance-authority/report 0.1.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/LICENSE +21 -0
  3. package/README.md +258 -0
  4. package/dist/changelog-message.d.ts +51 -0
  5. package/dist/changelog-message.js +244 -0
  6. package/dist/changelog-message.js.map +1 -0
  7. package/dist/changelog.d.ts +231 -0
  8. package/dist/changelog.js +96 -0
  9. package/dist/changelog.js.map +1 -0
  10. package/dist/cluster.d.ts +112 -0
  11. package/dist/cluster.js +109 -0
  12. package/dist/cluster.js.map +1 -0
  13. package/dist/composition.d.ts +248 -0
  14. package/dist/composition.js +33 -0
  15. package/dist/composition.js.map +1 -0
  16. package/dist/declarations.d.ts +266 -0
  17. package/dist/declarations.js +212 -0
  18. package/dist/declarations.js.map +1 -0
  19. package/dist/file.d.ts +28 -0
  20. package/dist/file.js +151 -0
  21. package/dist/file.js.map +1 -0
  22. package/dist/finding-record.d.ts +64 -0
  23. package/dist/finding-record.js +14 -0
  24. package/dist/finding-record.js.map +1 -0
  25. package/dist/findings.d.ts +157 -0
  26. package/dist/findings.js +227 -0
  27. package/dist/findings.js.map +1 -0
  28. package/dist/format.d.ts +444 -0
  29. package/dist/format.js +2 -0
  30. package/dist/format.js.map +1 -0
  31. package/dist/history-records.d.ts +112 -0
  32. package/dist/history-records.js +16 -0
  33. package/dist/history-records.js.map +1 -0
  34. package/dist/index.d.ts +45 -0
  35. package/dist/index.js +35 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/intent.d.ts +168 -0
  38. package/dist/intent.js +214 -0
  39. package/dist/intent.js.map +1 -0
  40. package/dist/presentation-record.d.ts +66 -0
  41. package/dist/presentation-record.js +9 -0
  42. package/dist/presentation-record.js.map +1 -0
  43. package/dist/promotion.d.ts +86 -0
  44. package/dist/promotion.js +104 -0
  45. package/dist/promotion.js.map +1 -0
  46. package/dist/reach.d.ts +154 -0
  47. package/dist/reach.js +47 -0
  48. package/dist/reach.js.map +1 -0
  49. package/dist/variation.d.ts +58 -0
  50. package/dist/variation.js +2 -0
  51. package/dist/variation.js.map +1 -0
  52. package/mark.svg +30 -0
  53. package/package.json +48 -0
@@ -0,0 +1,45 @@
1
+ /**
2
+ * `@variance-authority/report` — what a run leaves behind.
3
+ *
4
+ * The **record** tool kind, in a box of its own. A run produces values in memory
5
+ * and then the process ends; this is the shape the answers take so they can be
6
+ * read afterwards, from a different process, on a different machine, by whoever
7
+ * or whatever is asking.
8
+ *
9
+ * It lives here rather than with any of its readers because it has several, and
10
+ * a format owned by one of them bends towards that one. The CLI writes it, the
11
+ * MCP tools read it, a PR comment renders it, and none of those is the format's
12
+ * home. That was not a hypothetical: the shapes used to live in the MCP package,
13
+ * so the CLI depended on an agent protocol to describe its own output.
14
+ *
15
+ * It also holds the three derivations that belong to the format rather than to any
16
+ * reader. The first groups a run's changes into the *distinct things that
17
+ * happened*, so a token edit across forty stories is one decision rather than
18
+ * forty. The second reads those changes back against what the author said they
19
+ * were doing, which is the only way an artifact can report the edit that never
20
+ * landed. The third folds what a reviewer accepted into the record of *why a
21
+ * baseline is what it is*, which outlives this file — into a commit message where
22
+ * baselines are commits, into a row where they are rows. The CLI prints them, the
23
+ * MCP tools answer from them, and none of those owns them.
24
+ *
25
+ * The default entrypoint is the format and needs nothing.
26
+ * `@variance-authority/report/file` reads and writes it on a disk.
27
+ */
28
+ export type { RunReport, ObservationRecord, RegionRecord, FindingRecord, PresentationEffectEvidence, PresentationEffectRecord, PresentationEffectTransition, PresentationInformationRecord, PresentationSignalRecord, NotObserved, NotObservedKind, FlakinessRecord, ChurnRecord, DriftRecord, VariationRecord, } from './format.js';
29
+ export type { CompositionReport, ComponentRecord, EchoRecord, DivergenceRecord, PartingRecord, MovementRecord, } from './composition.js';
30
+ export type { ReachReport, ReachedComponent, ReachHole, SubjectReach, } from './reach.js';
31
+ export { absorbedBands, greenBecause, ignoreSays, ignoreState, ignoreShare, ignoreTotals, isActionable, sensitivitySays, sensitivityState, sensitivityShare, sensitivityTotals, } from './declarations.js';
32
+ export type { Green, GreenSubject, IgnoreLedger, IgnoreState, IgnoreUsage, SensitivityLedger, SensitivityState, SensitivityUsage, } from './declarations.js';
33
+ export { AGE_WHY, AGE_WORDS, ageOf, arrivalLine, bandTitle, byArrival, byBand, carriedLine, findingTotals, methodLine, mixedAges, } from './findings.js';
34
+ export type { Age, Arrival, Grouped } from './findings.js';
35
+ export { clusterChanges, describeClustering } from './cluster.js';
36
+ export type { Change, Clustering } from './cluster.js';
37
+ export { changelogOf, isRecorded } from './changelog.js';
38
+ export type { ChangelogDrift, ChangelogEntry, ChangelogOptions, ChangelogRecord, ChangelogSelection, Unrecordable, } from './changelog.js';
39
+ export { changelogBody, parseCommitMessage, renderCommitMessage, } from './changelog-message.js';
40
+ export type { CommitMessageOptions } from './changelog-message.js';
41
+ export { promotionOf, selectByShape, whyNotWhole } from './promotion.js';
42
+ export type { Promotion } from './promotion.js';
43
+ export { adjudicateRun, describeAdjudication, parseRoot } from './intent.js';
44
+ export type { AdjudicateOptions, Claim, ClaimOutcome, ClaimRootKind, ClaimVerdict, RunAdjudication, UnclaimedChange, } from './intent.js';
45
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * `@variance-authority/report` — what a run leaves behind.
3
+ *
4
+ * The **record** tool kind, in a box of its own. A run produces values in memory
5
+ * and then the process ends; this is the shape the answers take so they can be
6
+ * read afterwards, from a different process, on a different machine, by whoever
7
+ * or whatever is asking.
8
+ *
9
+ * It lives here rather than with any of its readers because it has several, and
10
+ * a format owned by one of them bends towards that one. The CLI writes it, the
11
+ * MCP tools read it, a PR comment renders it, and none of those is the format's
12
+ * home. That was not a hypothetical: the shapes used to live in the MCP package,
13
+ * so the CLI depended on an agent protocol to describe its own output.
14
+ *
15
+ * It also holds the three derivations that belong to the format rather than to any
16
+ * reader. The first groups a run's changes into the *distinct things that
17
+ * happened*, so a token edit across forty stories is one decision rather than
18
+ * forty. The second reads those changes back against what the author said they
19
+ * were doing, which is the only way an artifact can report the edit that never
20
+ * landed. The third folds what a reviewer accepted into the record of *why a
21
+ * baseline is what it is*, which outlives this file — into a commit message where
22
+ * baselines are commits, into a row where they are rows. The CLI prints them, the
23
+ * MCP tools answer from them, and none of those owns them.
24
+ *
25
+ * The default entrypoint is the format and needs nothing.
26
+ * `@variance-authority/report/file` reads and writes it on a disk.
27
+ */
28
+ export { absorbedBands, greenBecause, ignoreSays, ignoreState, ignoreShare, ignoreTotals, isActionable, sensitivitySays, sensitivityState, sensitivityShare, sensitivityTotals, } from './declarations.js';
29
+ export { AGE_WHY, AGE_WORDS, ageOf, arrivalLine, bandTitle, byArrival, byBand, carriedLine, findingTotals, methodLine, mixedAges, } from './findings.js';
30
+ export { clusterChanges, describeClustering } from './cluster.js';
31
+ export { changelogOf, isRecorded } from './changelog.js';
32
+ export { changelogBody, parseCommitMessage, renderCommitMessage, } from './changelog-message.js';
33
+ export { promotionOf, selectByShape, whyNotWhole } from './promotion.js';
34
+ export { adjudicateRun, describeAdjudication, parseRoot } from './intent.js';
35
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAoCH,OAAO,EACL,aAAa,EACb,YAAY,EACZ,UAAU,EACV,WAAW,EACX,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,mBAAmB,CAAC;AAY3B,OAAO,EACL,OAAO,EACP,SAAS,EACT,KAAK,EACL,WAAW,EACX,SAAS,EACT,SAAS,EACT,MAAM,EACN,WAAW,EACX,aAAa,EACb,UAAU,EACV,SAAS,GACV,MAAM,eAAe,CAAC;AAGvB,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAGlE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAUzD,OAAO,EACL,aAAa,EACb,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,wBAAwB,CAAC;AAGhC,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAGzE,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC","sourcesContent":["/**\n * `@variance-authority/report` — what a run leaves behind.\n *\n * The **record** tool kind, in a box of its own. A run produces values in memory\n * and then the process ends; this is the shape the answers take so they can be\n * read afterwards, from a different process, on a different machine, by whoever\n * or whatever is asking.\n *\n * It lives here rather than with any of its readers because it has several, and\n * a format owned by one of them bends towards that one. The CLI writes it, the\n * MCP tools read it, a PR comment renders it, and none of those is the format's\n * home. That was not a hypothetical: the shapes used to live in the MCP package,\n * so the CLI depended on an agent protocol to describe its own output.\n *\n * It also holds the three derivations that belong to the format rather than to any\n * reader. The first groups a run's changes into the *distinct things that\n * happened*, so a token edit across forty stories is one decision rather than\n * forty. The second reads those changes back against what the author said they\n * were doing, which is the only way an artifact can report the edit that never\n * landed. The third folds what a reviewer accepted into the record of *why a\n * baseline is what it is*, which outlives this file — into a commit message where\n * baselines are commits, into a row where they are rows. The CLI prints them, the\n * MCP tools answer from them, and none of those owns them.\n *\n * The default entrypoint is the format and needs nothing.\n * `@variance-authority/report/file` reads and writes it on a disk.\n */\n\nexport type {\n RunReport,\n ObservationRecord,\n RegionRecord,\n FindingRecord,\n PresentationEffectEvidence,\n PresentationEffectRecord,\n PresentationEffectTransition,\n PresentationInformationRecord,\n PresentationSignalRecord,\n NotObserved,\n NotObservedKind,\n FlakinessRecord,\n ChurnRecord,\n DriftRecord,\n VariationRecord,\n} from './format.js';\n\nexport type {\n CompositionReport,\n ComponentRecord,\n EchoRecord,\n DivergenceRecord,\n PartingRecord,\n MovementRecord,\n} from './composition.js';\n\nexport type {\n ReachReport,\n ReachedComponent,\n ReachHole,\n SubjectReach,\n} from './reach.js';\n\nexport {\n absorbedBands,\n greenBecause,\n ignoreSays,\n ignoreState,\n ignoreShare,\n ignoreTotals,\n isActionable,\n sensitivitySays,\n sensitivityState,\n sensitivityShare,\n sensitivityTotals,\n} from './declarations.js';\nexport type {\n Green,\n GreenSubject,\n IgnoreLedger,\n IgnoreState,\n IgnoreUsage,\n SensitivityLedger,\n SensitivityState,\n SensitivityUsage,\n} from './declarations.js';\n\nexport {\n AGE_WHY,\n AGE_WORDS,\n ageOf,\n arrivalLine,\n bandTitle,\n byArrival,\n byBand,\n carriedLine,\n findingTotals,\n methodLine,\n mixedAges,\n} from './findings.js';\nexport type { Age, Arrival, Grouped } from './findings.js';\n\nexport { clusterChanges, describeClustering } from './cluster.js';\nexport type { Change, Clustering } from './cluster.js';\n\nexport { changelogOf, isRecorded } from './changelog.js';\nexport type {\n ChangelogDrift,\n ChangelogEntry,\n ChangelogOptions,\n ChangelogRecord,\n ChangelogSelection,\n Unrecordable,\n} from './changelog.js';\n\nexport {\n changelogBody,\n parseCommitMessage,\n renderCommitMessage,\n} from './changelog-message.js';\nexport type { CommitMessageOptions } from './changelog-message.js';\n\nexport { promotionOf, selectByShape, whyNotWhole } from './promotion.js';\nexport type { Promotion } from './promotion.js';\n\nexport { adjudicateRun, describeAdjudication, parseRoot } from './intent.js';\nexport type {\n AdjudicateOptions,\n Claim,\n ClaimOutcome,\n ClaimRootKind,\n ClaimVerdict,\n RunAdjudication,\n UnclaimedChange,\n} from './intent.js';\n"]}
@@ -0,0 +1,168 @@
1
+ import type { Change } from './cluster.js';
2
+ import type { NotObserved, RunReport } from './format.js';
3
+ /**
4
+ * A claim, and what the run did with it.
5
+ *
6
+ * ## The half nobody asks for
7
+ *
8
+ * Everything else in this package answers *what changed*. That is not the
9
+ * question an agent that just edited a component is asking. It already knows what
10
+ * it meant to do; what it cannot know is whether the edit landed, whether it
11
+ * landed only where it was supposed to, and whether the thing it did not mean to
12
+ * touch moved anyway.
13
+ *
14
+ * So the agent declares first — *I am changing `Button`, paint only, at most 12
15
+ * subjects* — and the run is read back against the declaration. Three arms come
16
+ * out, and they are three different products:
17
+ *
18
+ * - **Delivered.** The change happened where it was declared. Confirmation, and
19
+ * the cheapest of the three.
20
+ * - **Unclaimed.** Something moved that no claim covers. The collateral to fix,
21
+ * or to name in the proposal before a human finds it.
22
+ * - **Undelivered.** A claim that matched nothing. *The edit did not take* —
23
+ * wrong file, dead branch, a rule something else overrides, a stale build.
24
+ *
25
+ * The third arm is the one aimed at the failure mode an agent actually has:
26
+ * acting, observing nothing, and proceeding on the belief that the action landed.
27
+ * A picture cannot produce it. A build with no diff and a build where the edit
28
+ * never ran are the same image, and every tool whose output is a score over two
29
+ * PNGs reports them identically.
30
+ *
31
+ * It also disciplines the other two. An agent that claims everything so nothing
32
+ * can ever be unclaimed walks straight into arm three, because a claim with no
33
+ * matching change is reported rather than absorbed. Over-claiming is visible by
34
+ * construction, not by policy.
35
+ *
36
+ * ## Declared before, not after
37
+ *
38
+ * A claim read out of the diff is not a claim. This function takes the
39
+ * declaration as an input and never derives one, which is what keeps the
40
+ * adjudication from scoring the run against itself.
41
+ *
42
+ * ## Why an undelivered claim has two different answers
43
+ *
44
+ * `Button` was claimed and nothing about `Button` changed. That is either
45
+ *
46
+ * - the run rendered `Button` — in this many subjects — and it did not move, so
47
+ * the edit did not take; or
48
+ * - the run never rendered `Button` at all, so the claim was not checked and
49
+ * nothing here may be read as evidence about it.
50
+ *
51
+ * Those are opposite instructions. The first sends an agent back to its own edit;
52
+ * the second sends it to the subject list, or to the provenance that would have
53
+ * named the component. The census in
54
+ * [`CompositionReport`](./composition.ts) is what separates them, and where a run
55
+ * kept no census this says *that*, rather than collapsing the pair into the
56
+ * reading that happens to be more flattering.
57
+ *
58
+ * ## The relationship to core's `adjudicate`
59
+ *
60
+ * `@variance-authority/core` adjudicates a **docket** — roots with bands,
61
+ * impacts, and a project policy — which exists while a run is in memory. A run
62
+ * report keeps no roots (see [`format.ts`](./format.ts): what is kept is what a
63
+ * sentence needs), so this adjudicates at the resolution the artifact actually
64
+ * recorded: the component and the shape. It is the same three arms against less
65
+ * evidence, and the fields it cannot check it names instead of ignoring.
66
+ */
67
+ /** What a claim may be made about, at the resolution a run report keeps. */
68
+ export type ClaimRootKind = 'component' | 'shape';
69
+ export interface Claim {
70
+ /**
71
+ * What the change is about: `component:Button`, `shape:<fingerprint>`, or a
72
+ * bare name read as a component.
73
+ *
74
+ * Matched exactly. A prefix match would let a claim about `Button` silently
75
+ * authorize a change to `ButtonGroup`, which is the one thing a declaration
76
+ * must never do.
77
+ */
78
+ readonly root: string;
79
+ /** Why. Never matched on; carried into the answer so a reader has the context. */
80
+ readonly reason: string;
81
+ /**
82
+ * Cap on how many subjects the change may reach.
83
+ *
84
+ * The most useful field in practice, and the one nobody checks unaided. "I
85
+ * meant to restyle `Button`" is almost always true; "I meant to restyle
86
+ * `Button` in 3 subjects" is the part that turns out to be 40.
87
+ */
88
+ readonly maxSubjects?: number;
89
+ }
90
+ export type ClaimVerdict =
91
+ /** Matched, within its declared bound. */
92
+ 'delivered'
93
+ /** Matched, and reached further than declared. Still the intended change. */
94
+ | 'overreached'
95
+ /** Matched nothing, and the run watched it. The edit did not take. */
96
+ | 'undelivered'
97
+ /** Matched nothing, and the run could not have seen it either way. */
98
+ | 'unobservable';
99
+ export interface ClaimOutcome {
100
+ readonly claim: Claim;
101
+ readonly verdict: ClaimVerdict;
102
+ /** Changes this claim accounts for, most widespread first. */
103
+ readonly changes: readonly Change[];
104
+ /** Subjects reached, deduplicated across the matched changes. */
105
+ readonly subjects: readonly string[];
106
+ /** One sentence, ready to print, naming the next move rather than the state. */
107
+ readonly because: string;
108
+ }
109
+ export interface UnclaimedChange {
110
+ readonly change: Change;
111
+ readonly because: string;
112
+ }
113
+ export interface RunAdjudication {
114
+ readonly claims: readonly ClaimOutcome[];
115
+ /** Changes no claim covers. Arm two, in report order. */
116
+ readonly unclaimed: readonly UnclaimedChange[];
117
+ /**
118
+ * Changed subjects that produced no shape at all.
119
+ *
120
+ * Never folded into either arm. A run with no document behind its comparison
121
+ * cannot say what moved, so it cannot say whether a claim covers it — and
122
+ * counting these as unclaimed would invent collateral out of missing evidence.
123
+ */
124
+ readonly ungrouped: readonly string[];
125
+ /**
126
+ * Subjects the run meant to observe and did not.
127
+ *
128
+ * Carried because it bounds every other line here. A claim checked against a
129
+ * run that failed on 50 of 300 subjects is a claim checked against 250, and an
130
+ * agent told `delivered` without being told that has been told something the
131
+ * run never established.
132
+ */
133
+ readonly blind: readonly NotObserved[];
134
+ /**
135
+ * The worst thing present, in the order an agent should act on it.
136
+ *
137
+ * `unmet` outranks `review`: a claim that did not land means the agent's own
138
+ * last action failed, and there is no point triaging collateral from an edit
139
+ * that never happened.
140
+ */
141
+ readonly verdict: 'clean' | 'review' | 'unmet';
142
+ }
143
+ export interface AdjudicateOptions {
144
+ /**
145
+ * Claim fields the caller supplied that this resolution cannot check.
146
+ *
147
+ * The boundary that parses agent input passes them through so the answer can
148
+ * say so. Silently dropping a declared band would tell an agent its band claim
149
+ * held when nothing looked at it.
150
+ */
151
+ readonly unchecked?: readonly string[];
152
+ }
153
+ /** Adjudicate a run against what its author said they were doing. */
154
+ export declare function adjudicateRun(report: RunReport, claims: readonly Claim[], options?: AdjudicateOptions): RunAdjudication;
155
+ /** `component:Button`, `shape:ab12…`, or a bare name read as a component. */
156
+ export declare function parseRoot(root: string): {
157
+ kind: ClaimRootKind;
158
+ name: string;
159
+ };
160
+ /**
161
+ * The adjudication as the thing an agent reads before deciding what to do next.
162
+ *
163
+ * Ordered by what it should act on, not by what the run computed first:
164
+ * undelivered claims lead, because an agent triaging collateral from an edit that
165
+ * never happened is an agent about to make its second mistake.
166
+ */
167
+ export declare function describeAdjudication(result: RunAdjudication): string;
168
+ //# sourceMappingURL=intent.d.ts.map
package/dist/intent.js ADDED
@@ -0,0 +1,214 @@
1
+ import { clusterChanges } from './cluster.js';
2
+ /** Adjudicate a run against what its author said they were doing. */
3
+ export function adjudicateRun(report, claims, options = {}) {
4
+ const clustering = clusterChanges(report.observations);
5
+ const census = componentCensus(report);
6
+ const taken = new Set();
7
+ const outcomes = claims.map((claim) => {
8
+ const matched = clustering.changes.filter((change) => covers(claim, change));
9
+ for (const change of matched)
10
+ taken.add(change);
11
+ return outcomeOf(claim, matched, census, options);
12
+ });
13
+ const unclaimed = clustering.changes
14
+ .filter((change) => !taken.has(change))
15
+ .map((change) => ({ change, because: unclaimedBecause(change) }));
16
+ return {
17
+ claims: outcomes,
18
+ unclaimed,
19
+ ungrouped: clustering.ungrouped,
20
+ blind: report.notObserved ?? [],
21
+ verdict: worst(outcomes, unclaimed, clustering),
22
+ };
23
+ }
24
+ /**
25
+ * Subjects each component was rendered in, or `undefined` when the run kept no
26
+ * census.
27
+ *
28
+ * `undefined` and an empty map are different claims and both occur: a run with no
29
+ * semantic snapshots to fold has no composition section at all, and reading that
30
+ * as "no component rendered anywhere" would turn every claim into evidence of a
31
+ * failed edit.
32
+ */
33
+ function componentCensus(report) {
34
+ const components = report.composition?.components;
35
+ if (components === undefined)
36
+ return undefined;
37
+ return new Map(components.map((record) => [record.component, record.subjects]));
38
+ }
39
+ function covers(claim, change) {
40
+ const { kind, name } = parseRoot(claim.root);
41
+ return kind === 'shape' ? change.fingerprint === name : change.component === name;
42
+ }
43
+ /** `component:Button`, `shape:ab12…`, or a bare name read as a component. */
44
+ export function parseRoot(root) {
45
+ const separator = root.indexOf(':');
46
+ if (separator === -1)
47
+ return { kind: 'component', name: root };
48
+ const prefix = root.slice(0, separator);
49
+ const name = root.slice(separator + 1);
50
+ return prefix === 'shape'
51
+ ? { kind: 'shape', name }
52
+ : { kind: 'component', name: prefix === 'component' ? name : root };
53
+ }
54
+ function outcomeOf(claim, matched, census, options) {
55
+ const subjects = [...new Set(matched.flatMap((change) => change.subjects))];
56
+ const caveat = options.unchecked?.length ? ` Not checked here: ${options.unchecked.join(', ')}.` : '';
57
+ if (matched.length === 0)
58
+ return undeliveredOutcome(claim, census, caveat);
59
+ const exceeded = claim.maxSubjects !== undefined && subjects.length > claim.maxSubjects;
60
+ const where = `${claim.root} changed in ${subjects.length} subject(s): ${preview(subjects)}`;
61
+ return exceeded
62
+ ? {
63
+ claim,
64
+ verdict: 'overreached',
65
+ changes: matched,
66
+ subjects,
67
+ // Not a failure: the change is the declared one. What missed is the
68
+ // author's estimate of its blast radius, which is the half worth showing
69
+ // them rather than blocking on.
70
+ because: `declared (${claim.reason}) and delivered, but reached ${subjects.length} ` +
71
+ `subject(s) against the ${claim.maxSubjects} declared — the change is the ` +
72
+ `intended one, its reach is not.${caveat}`,
73
+ }
74
+ : {
75
+ claim,
76
+ verdict: 'delivered',
77
+ changes: matched,
78
+ subjects,
79
+ because: `declared (${claim.reason}) and delivered: ${where}.${caveat}`,
80
+ };
81
+ }
82
+ /** Arm three, split by whether the run was in a position to see the claim fail. */
83
+ function undeliveredOutcome(claim, census, caveat) {
84
+ const { kind, name } = parseRoot(claim.root);
85
+ const blank = { claim, changes: [], subjects: [] };
86
+ if (kind === 'shape') {
87
+ return {
88
+ ...blank,
89
+ verdict: 'undelivered',
90
+ // A shape is a digest of a difference. Claiming one that did not occur is
91
+ // always the strong reading: nothing else could have produced it.
92
+ because: `declared (${claim.reason}) and no change in this run carries shape ` +
93
+ `\`${name}\` — nothing produced that difference.${caveat}`,
94
+ };
95
+ }
96
+ if (census === undefined) {
97
+ return {
98
+ ...blank,
99
+ verdict: 'unobservable',
100
+ because: `declared (${claim.reason}) and nothing changed under \`${name}\`, but this run ` +
101
+ 'kept no component census, so it cannot say whether `' +
102
+ name +
103
+ '` was rendered at all. The claim was not checked.' +
104
+ caveat,
105
+ };
106
+ }
107
+ const rendered = census.get(name);
108
+ if (rendered === undefined) {
109
+ return {
110
+ ...blank,
111
+ verdict: 'unobservable',
112
+ because: `declared (${claim.reason}) and this run never rendered \`${name}\` in any subject, ` +
113
+ 'so nothing here is evidence about it either way. Check the subject selection, or ' +
114
+ 'whether provenance names this component.' +
115
+ caveat,
116
+ };
117
+ }
118
+ return {
119
+ ...blank,
120
+ verdict: 'undelivered',
121
+ because: `declared (${claim.reason}) and \`${name}\` rendered in ${rendered.length} subject(s) ` +
122
+ `— ${preview(rendered)} — and did not change. The edit did not take: wrong file, a ` +
123
+ 'dead branch, a rule something else overrides, or a stale build.' +
124
+ caveat,
125
+ };
126
+ }
127
+ function unclaimedBecause(change) {
128
+ const named = change.component ?? '(no component resolved; grouped by shape alone)';
129
+ const settle = change.settles.length === 0
130
+ ? 'nothing it can settle on its own'
131
+ : `settles ${change.settles.length} of them`;
132
+ return (`${named} moved and no claim covers it — ${change.subjects.length} subject(s), ` +
133
+ `${change.pixels} pixel(s), ${settle}` +
134
+ (change.cause ? '' : '; it was displaced rather than edited'));
135
+ }
136
+ function worst(outcomes, unclaimed, clustering) {
137
+ if (outcomes.some((outcome) => outcome.verdict === 'undelivered'))
138
+ return 'unmet';
139
+ if (unclaimed.length > 0 || clustering.ungrouped.length > 0)
140
+ return 'review';
141
+ if (outcomes.some((outcome) => outcome.verdict !== 'delivered'))
142
+ return 'review';
143
+ return 'clean';
144
+ }
145
+ /**
146
+ * The adjudication as the thing an agent reads before deciding what to do next.
147
+ *
148
+ * Ordered by what it should act on, not by what the run computed first:
149
+ * undelivered claims lead, because an agent triaging collateral from an edit that
150
+ * never happened is an agent about to make its second mistake.
151
+ */
152
+ export function describeAdjudication(result) {
153
+ const counts = tally(result);
154
+ const lines = [headline(result, counts)];
155
+ const order = ['undelivered', 'unobservable', 'overreached', 'delivered'];
156
+ for (const verdict of order) {
157
+ for (const outcome of result.claims.filter((claim) => claim.verdict === verdict)) {
158
+ lines.push('', ` [${outcome.verdict}] ${outcome.claim.root}`, ` ${outcome.because}`);
159
+ for (const change of outcome.changes) {
160
+ if (change.file !== undefined)
161
+ lines.push(` ${change.file}`);
162
+ }
163
+ }
164
+ }
165
+ for (const entry of result.unclaimed) {
166
+ lines.push('', ` [unclaimed] ${entry.change.component ?? entry.change.fingerprint}`);
167
+ lines.push(` ${entry.because}`);
168
+ if (entry.change.file !== undefined)
169
+ lines.push(` ${entry.change.file}`);
170
+ if (entry.change.settles.length > 0) {
171
+ lines.push(` variance accept --shape ${entry.change.fingerprint}`);
172
+ }
173
+ }
174
+ if (result.ungrouped.length > 0) {
175
+ lines.push('', ` [ungrouped] ${result.ungrouped.length} changed subject(s) carry no shape, so no claim ` +
176
+ 'could be checked against them: ' +
177
+ preview(result.ungrouped));
178
+ }
179
+ if (result.blind.length > 0) {
180
+ lines.push('', ` [not observed] ${result.blind.length} subject(s) were not looked at, so every line ` +
181
+ 'above is bounded by what this run saw: ' +
182
+ preview(result.blind.map((entry) => entry.subject)));
183
+ }
184
+ return lines.join('\n');
185
+ }
186
+ function headline(result, counts) {
187
+ const claims = `${result.claims.length} claim(s): ${counts.delivered} delivered, ` +
188
+ `${counts.undelivered} undelivered, ${counts.overreached} over-reaching, ` +
189
+ `${counts.unobservable} unchecked.`;
190
+ const lead = result.verdict === 'unmet'
191
+ ? 'An edit you declared did not take. Fix that before reading anything else.'
192
+ : result.verdict === 'review'
193
+ ? 'Every declared edit landed; something you did not declare also moved.'
194
+ : 'Every declared edit landed, and nothing else moved.';
195
+ return `${lead}\n${claims} ${result.unclaimed.length} unclaimed change(s).`;
196
+ }
197
+ function tally(result) {
198
+ const counts = {
199
+ delivered: 0,
200
+ overreached: 0,
201
+ undelivered: 0,
202
+ unobservable: 0,
203
+ };
204
+ for (const outcome of result.claims)
205
+ counts[outcome.verdict] += 1;
206
+ return counts;
207
+ }
208
+ /** The first few, because a reader wants examples and not a manifest. */
209
+ function preview(items) {
210
+ return items.length <= 4
211
+ ? items.join(', ')
212
+ : `${items.slice(0, 4).join(', ')}, and ${items.length - 4} more`;
213
+ }
214
+ //# sourceMappingURL=intent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"intent.js","sourceRoot":"","sources":["../src/intent.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAsK9C,qEAAqE;AACrE,MAAM,UAAU,aAAa,CAC3B,MAAiB,EACjB,MAAwB,EACxB,OAAO,GAAsB,EAAE;IAE/B,MAAM,UAAU,GAAG,cAAc,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;IACvD,MAAM,MAAM,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;IAEvC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;QACpC,MAAM,OAAO,GAAG,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC;QAC7E,KAAK,MAAM,MAAM,IAAI,OAAO;YAAE,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAChD,OAAO,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;IACpD,CAAC,CAAC,CAAC;IAEH,MAAM,SAAS,GAAG,UAAU,CAAC,OAAO;SACjC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;SACtC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;IAEpE,OAAO;QACL,MAAM,EAAE,QAAQ;QAChB,SAAS;QACT,SAAS,EAAE,UAAU,CAAC,SAAS;QAC/B,KAAK,EAAE,MAAM,CAAC,WAAW,IAAI,EAAE;QAC/B,OAAO,EAAE,KAAK,CAAC,QAAQ,EAAE,SAAS,EAAE,UAAU,CAAC;KAChD,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,eAAe,CAAC,MAAiB;IACxC,MAAM,UAAU,GAAG,MAAM,CAAC,WAAW,EAAE,UAAU,CAAC;IAClD,IAAI,UAAU,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAE/C,OAAO,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;AAClF,CAAC;AAED,SAAS,MAAM,CAAC,KAAY,EAAE,MAAc;IAC1C,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAE7C,OAAO,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,KAAK,IAAI,CAAC;AACpF,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACpC,IAAI,SAAS,KAAK,CAAC,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAE/D,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;IACxC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC;IAEvC,OAAO,MAAM,KAAK,OAAO;QACvB,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE;QACzB,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;AACxE,CAAC;AAED,SAAS,SAAS,CAChB,KAAY,EACZ,OAA0B,EAC1B,MAAkD,EAClD,OAA0B;IAE1B,MAAM,QAAQ,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;IAC5E,MAAM,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC,sBAAsB,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IAEtG,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAE3E,MAAM,QAAQ,GAAG,KAAK,CAAC,WAAW,KAAK,SAAS,IAAI,QAAQ,CAAC,MAAM,GAAG,KAAK,CAAC,WAAW,CAAC;IACxF,MAAM,KAAK,GAAG,GAAG,KAAK,CAAC,IAAI,eAAe,QAAQ,CAAC,MAAM,gBAAgB,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;IAE7F,OAAO,QAAQ;QACb,CAAC,CAAC;YACE,KAAK;YACL,OAAO,EAAE,aAAa;YACtB,OAAO,EAAE,OAAO;YAChB,QAAQ;YACR,oEAAoE;YACpE,yEAAyE;YACzE,gCAAgC;YAChC,OAAO,EACL,aAAa,KAAK,CAAC,MAAM,gCAAgC,QAAQ,CAAC,MAAM,GAAG;gBAC3E,0BAA0B,KAAK,CAAC,WAAW,gCAAgC;gBAC3E,kCAAkC,MAAM,EAAE;SAC7C;QACH,CAAC,CAAC;YACE,KAAK;YACL,OAAO,EAAE,WAAW;YACpB,OAAO,EAAE,OAAO;YAChB,QAAQ;YACR,OAAO,EAAE,aAAa,KAAK,CAAC,MAAM,oBAAoB,KAAK,IAAI,MAAM,EAAE;SACxE,CAAC;AACR,CAAC;AAED,mFAAmF;AACnF,SAAS,kBAAkB,CACzB,KAAY,EACZ,MAAkD,EAClD,MAAc;IAEd,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC7C,MAAM,KAAK,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAW,CAAC;IAE5D,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;QACrB,OAAO;YACL,GAAG,KAAK;YACR,OAAO,EAAE,aAAa;YACtB,0EAA0E;YAC1E,kEAAkE;YAClE,OAAO,EACL,aAAa,KAAK,CAAC,MAAM,4CAA4C;gBACrE,KAAK,IAAI,yCAAyC,MAAM,EAAE;SAC7D,CAAC;IACJ,CAAC;IAED,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO;YACL,GAAG,KAAK;YACR,OAAO,EAAE,cAAc;YACvB,OAAO,EACL,aAAa,KAAK,CAAC,MAAM,iCAAiC,IAAI,mBAAmB;gBACjF,sDAAsD;gBACtD,IAAI;gBACJ,mDAAmD;gBACnD,MAAM;SACT,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAElC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,OAAO;YACL,GAAG,KAAK;YACR,OAAO,EAAE,cAAc;YACvB,OAAO,EACL,aAAa,KAAK,CAAC,MAAM,mCAAmC,IAAI,qBAAqB;gBACrF,mFAAmF;gBACnF,0CAA0C;gBAC1C,MAAM;SACT,CAAC;IACJ,CAAC;IAED,OAAO;QACL,GAAG,KAAK;QACR,OAAO,EAAE,aAAa;QACtB,OAAO,EACL,aAAa,KAAK,CAAC,MAAM,WAAW,IAAI,kBAAkB,QAAQ,CAAC,MAAM,cAAc;YACvF,KAAK,OAAO,CAAC,QAAQ,CAAC,8DAA8D;YACpF,iEAAiE;YACjE,MAAM;KACT,CAAC;AACJ,CAAC;AAED,SAAS,gBAAgB,CAAC,MAAc;IACtC,MAAM,KAAK,GAAG,MAAM,CAAC,SAAS,IAAI,iDAAiD,CAAC;IACpF,MAAM,MAAM,GACV,MAAM,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC;QACzB,CAAC,CAAC,kCAAkC;QACpC,CAAC,CAAC,WAAW,MAAM,CAAC,OAAO,CAAC,MAAM,UAAU,CAAC;IAEjD,OAAO,CACL,GAAG,KAAK,mCAAmC,MAAM,CAAC,QAAQ,CAAC,MAAM,eAAe;QAChF,GAAG,MAAM,CAAC,MAAM,cAAc,MAAM,EAAE;QACtC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,uCAAuC,CAAC,CAC9D,CAAC;AACJ,CAAC;AAED,SAAS,KAAK,CACZ,QAAiC,EACjC,SAAqC,EACrC,UAAsB;IAEtB,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,KAAK,aAAa,CAAC;QAAE,OAAO,OAAO,CAAC;IAClF,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,IAAI,UAAU,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,QAAQ,CAAC;IAC7E,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,KAAK,WAAW,CAAC;QAAE,OAAO,QAAQ,CAAC;IACjF,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAAC,MAAuB;IAC1D,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;IAC7B,MAAM,KAAK,GAAa,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAEnD,MAAM,KAAK,GAA4B,CAAC,aAAa,EAAE,cAAc,EAAE,aAAa,EAAE,WAAW,CAAC,CAAC;IACnG,KAAK,MAAM,OAAO,IAAI,KAAK,EAAE,CAAC;QAC5B,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,KAAK,OAAO,CAAC,EAAE,CAAC;YACjF,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,MAAM,OAAO,CAAC,OAAO,KAAK,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,SAAS,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;YAC3F,KAAK,MAAM,MAAM,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;gBACrC,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS;oBAAE,KAAK,CAAC,IAAI,CAAC,SAAS,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;YACpE,CAAC;QACH,CAAC;IACH,CAAC;IAED,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;QACrC,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,iBAAiB,KAAK,CAAC,MAAM,CAAC,SAAS,IAAI,KAAK,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;QACtF,KAAK,CAAC,IAAI,CAAC,SAAS,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QACrC,IAAI,KAAK,CAAC,MAAM,CAAC,IAAI,KAAK,SAAS;YAAE,KAAK,CAAC,IAAI,CAAC,SAAS,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;QAC9E,IAAI,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACpC,KAAK,CAAC,IAAI,CAAC,iCAAiC,KAAK,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;QAC1E,CAAC;IACH,CAAC;IAED,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAChC,KAAK,CAAC,IAAI,CACR,EAAE,EACF,iBAAiB,MAAM,CAAC,SAAS,CAAC,MAAM,kDAAkD;YACxF,iCAAiC;YACjC,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,CAC5B,CAAC;IACJ,CAAC;IAED,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC5B,KAAK,CAAC,IAAI,CACR,EAAE,EACF,oBAAoB,MAAM,CAAC,KAAK,CAAC,MAAM,gDAAgD;YACrF,yCAAyC;YACzC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CACtD,CAAC;IACJ,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,SAAS,QAAQ,CAAC,MAAuB,EAAE,MAAoC;IAC7E,MAAM,MAAM,GACV,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,cAAc,MAAM,CAAC,SAAS,cAAc;QACnE,GAAG,MAAM,CAAC,WAAW,iBAAiB,MAAM,CAAC,WAAW,kBAAkB;QAC1E,GAAG,MAAM,CAAC,YAAY,aAAa,CAAC;IAEtC,MAAM,IAAI,GACR,MAAM,CAAC,OAAO,KAAK,OAAO;QACxB,CAAC,CAAC,2EAA2E;QAC7E,CAAC,CAAC,MAAM,CAAC,OAAO,KAAK,QAAQ;YAC3B,CAAC,CAAC,uEAAuE;YACzE,CAAC,CAAC,qDAAqD,CAAC;IAE9D,OAAO,GAAG,IAAI,KAAK,MAAM,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,uBAAuB,CAAC;AAC9E,CAAC;AAED,SAAS,KAAK,CAAC,MAAuB;IACpC,MAAM,MAAM,GAAiC;QAC3C,SAAS,EAAE,CAAC;QACZ,WAAW,EAAE,CAAC;QACd,WAAW,EAAE,CAAC;QACd,YAAY,EAAE,CAAC;KAChB,CAAC;IACF,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,MAAM;QAAE,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,yEAAyE;AACzE,SAAS,OAAO,CAAC,KAAwB;IACvC,OAAO,KAAK,CAAC,MAAM,IAAI,CAAC;QACtB,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC;QAClB,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,KAAK,CAAC,MAAM,GAAG,CAAC,OAAO,CAAC;AACtE,CAAC","sourcesContent":["import type { Change, Clustering } from './cluster.js';\nimport { clusterChanges } from './cluster.js';\nimport type { NotObserved, RunReport } from './format.js';\n\n/**\n * A claim, and what the run did with it.\n *\n * ## The half nobody asks for\n *\n * Everything else in this package answers *what changed*. That is not the\n * question an agent that just edited a component is asking. It already knows what\n * it meant to do; what it cannot know is whether the edit landed, whether it\n * landed only where it was supposed to, and whether the thing it did not mean to\n * touch moved anyway.\n *\n * So the agent declares first — *I am changing `Button`, paint only, at most 12\n * subjects* — and the run is read back against the declaration. Three arms come\n * out, and they are three different products:\n *\n * - **Delivered.** The change happened where it was declared. Confirmation, and\n * the cheapest of the three.\n * - **Unclaimed.** Something moved that no claim covers. The collateral to fix,\n * or to name in the proposal before a human finds it.\n * - **Undelivered.** A claim that matched nothing. *The edit did not take* —\n * wrong file, dead branch, a rule something else overrides, a stale build.\n *\n * The third arm is the one aimed at the failure mode an agent actually has:\n * acting, observing nothing, and proceeding on the belief that the action landed.\n * A picture cannot produce it. A build with no diff and a build where the edit\n * never ran are the same image, and every tool whose output is a score over two\n * PNGs reports them identically.\n *\n * It also disciplines the other two. An agent that claims everything so nothing\n * can ever be unclaimed walks straight into arm three, because a claim with no\n * matching change is reported rather than absorbed. Over-claiming is visible by\n * construction, not by policy.\n *\n * ## Declared before, not after\n *\n * A claim read out of the diff is not a claim. This function takes the\n * declaration as an input and never derives one, which is what keeps the\n * adjudication from scoring the run against itself.\n *\n * ## Why an undelivered claim has two different answers\n *\n * `Button` was claimed and nothing about `Button` changed. That is either\n *\n * - the run rendered `Button` — in this many subjects — and it did not move, so\n * the edit did not take; or\n * - the run never rendered `Button` at all, so the claim was not checked and\n * nothing here may be read as evidence about it.\n *\n * Those are opposite instructions. The first sends an agent back to its own edit;\n * the second sends it to the subject list, or to the provenance that would have\n * named the component. The census in\n * [`CompositionReport`](./composition.ts) is what separates them, and where a run\n * kept no census this says *that*, rather than collapsing the pair into the\n * reading that happens to be more flattering.\n *\n * ## The relationship to core's `adjudicate`\n *\n * `@variance-authority/core` adjudicates a **docket** — roots with bands,\n * impacts, and a project policy — which exists while a run is in memory. A run\n * report keeps no roots (see [`format.ts`](./format.ts): what is kept is what a\n * sentence needs), so this adjudicates at the resolution the artifact actually\n * recorded: the component and the shape. It is the same three arms against less\n * evidence, and the fields it cannot check it names instead of ignoring.\n */\n\n/** What a claim may be made about, at the resolution a run report keeps. */\nexport type ClaimRootKind = 'component' | 'shape';\n\nexport interface Claim {\n /**\n * What the change is about: `component:Button`, `shape:<fingerprint>`, or a\n * bare name read as a component.\n *\n * Matched exactly. A prefix match would let a claim about `Button` silently\n * authorize a change to `ButtonGroup`, which is the one thing a declaration\n * must never do.\n */\n readonly root: string;\n\n /** Why. Never matched on; carried into the answer so a reader has the context. */\n readonly reason: string;\n\n /**\n * Cap on how many subjects the change may reach.\n *\n * The most useful field in practice, and the one nobody checks unaided. \"I\n * meant to restyle `Button`\" is almost always true; \"I meant to restyle\n * `Button` in 3 subjects\" is the part that turns out to be 40.\n */\n readonly maxSubjects?: number;\n}\n\nexport type ClaimVerdict =\n /** Matched, within its declared bound. */\n | 'delivered'\n /** Matched, and reached further than declared. Still the intended change. */\n | 'overreached'\n /** Matched nothing, and the run watched it. The edit did not take. */\n | 'undelivered'\n /** Matched nothing, and the run could not have seen it either way. */\n | 'unobservable';\n\nexport interface ClaimOutcome {\n readonly claim: Claim;\n readonly verdict: ClaimVerdict;\n /** Changes this claim accounts for, most widespread first. */\n readonly changes: readonly Change[];\n /** Subjects reached, deduplicated across the matched changes. */\n readonly subjects: readonly string[];\n /** One sentence, ready to print, naming the next move rather than the state. */\n readonly because: string;\n}\n\nexport interface UnclaimedChange {\n readonly change: Change;\n readonly because: string;\n}\n\nexport interface RunAdjudication {\n readonly claims: readonly ClaimOutcome[];\n /** Changes no claim covers. Arm two, in report order. */\n readonly unclaimed: readonly UnclaimedChange[];\n\n /**\n * Changed subjects that produced no shape at all.\n *\n * Never folded into either arm. A run with no document behind its comparison\n * cannot say what moved, so it cannot say whether a claim covers it — and\n * counting these as unclaimed would invent collateral out of missing evidence.\n */\n readonly ungrouped: readonly string[];\n\n /**\n * Subjects the run meant to observe and did not.\n *\n * Carried because it bounds every other line here. A claim checked against a\n * run that failed on 50 of 300 subjects is a claim checked against 250, and an\n * agent told `delivered` without being told that has been told something the\n * run never established.\n */\n readonly blind: readonly NotObserved[];\n\n /**\n * The worst thing present, in the order an agent should act on it.\n *\n * `unmet` outranks `review`: a claim that did not land means the agent's own\n * last action failed, and there is no point triaging collateral from an edit\n * that never happened.\n */\n readonly verdict: 'clean' | 'review' | 'unmet';\n}\n\nexport interface AdjudicateOptions {\n /**\n * Claim fields the caller supplied that this resolution cannot check.\n *\n * The boundary that parses agent input passes them through so the answer can\n * say so. Silently dropping a declared band would tell an agent its band claim\n * held when nothing looked at it.\n */\n readonly unchecked?: readonly string[];\n}\n\n/** Adjudicate a run against what its author said they were doing. */\nexport function adjudicateRun(\n report: RunReport,\n claims: readonly Claim[],\n options: AdjudicateOptions = {},\n): RunAdjudication {\n const clustering = clusterChanges(report.observations);\n const census = componentCensus(report);\n\n const taken = new Set<Change>();\n const outcomes = claims.map((claim) => {\n const matched = clustering.changes.filter((change) => covers(claim, change));\n for (const change of matched) taken.add(change);\n return outcomeOf(claim, matched, census, options);\n });\n\n const unclaimed = clustering.changes\n .filter((change) => !taken.has(change))\n .map((change) => ({ change, because: unclaimedBecause(change) }));\n\n return {\n claims: outcomes,\n unclaimed,\n ungrouped: clustering.ungrouped,\n blind: report.notObserved ?? [],\n verdict: worst(outcomes, unclaimed, clustering),\n };\n}\n\n/**\n * Subjects each component was rendered in, or `undefined` when the run kept no\n * census.\n *\n * `undefined` and an empty map are different claims and both occur: a run with no\n * semantic snapshots to fold has no composition section at all, and reading that\n * as \"no component rendered anywhere\" would turn every claim into evidence of a\n * failed edit.\n */\nfunction componentCensus(report: RunReport): Map<string, readonly string[]> | undefined {\n const components = report.composition?.components;\n if (components === undefined) return undefined;\n\n return new Map(components.map((record) => [record.component, record.subjects]));\n}\n\nfunction covers(claim: Claim, change: Change): boolean {\n const { kind, name } = parseRoot(claim.root);\n\n return kind === 'shape' ? change.fingerprint === name : change.component === name;\n}\n\n/** `component:Button`, `shape:ab12…`, or a bare name read as a component. */\nexport function parseRoot(root: string): { kind: ClaimRootKind; name: string } {\n const separator = root.indexOf(':');\n if (separator === -1) return { kind: 'component', name: root };\n\n const prefix = root.slice(0, separator);\n const name = root.slice(separator + 1);\n\n return prefix === 'shape'\n ? { kind: 'shape', name }\n : { kind: 'component', name: prefix === 'component' ? name : root };\n}\n\nfunction outcomeOf(\n claim: Claim,\n matched: readonly Change[],\n census: Map<string, readonly string[]> | undefined,\n options: AdjudicateOptions,\n): ClaimOutcome {\n const subjects = [...new Set(matched.flatMap((change) => change.subjects))];\n const caveat = options.unchecked?.length ? ` Not checked here: ${options.unchecked.join(', ')}.` : '';\n\n if (matched.length === 0) return undeliveredOutcome(claim, census, caveat);\n\n const exceeded = claim.maxSubjects !== undefined && subjects.length > claim.maxSubjects;\n const where = `${claim.root} changed in ${subjects.length} subject(s): ${preview(subjects)}`;\n\n return exceeded\n ? {\n claim,\n verdict: 'overreached',\n changes: matched,\n subjects,\n // Not a failure: the change is the declared one. What missed is the\n // author's estimate of its blast radius, which is the half worth showing\n // them rather than blocking on.\n because:\n `declared (${claim.reason}) and delivered, but reached ${subjects.length} ` +\n `subject(s) against the ${claim.maxSubjects} declared — the change is the ` +\n `intended one, its reach is not.${caveat}`,\n }\n : {\n claim,\n verdict: 'delivered',\n changes: matched,\n subjects,\n because: `declared (${claim.reason}) and delivered: ${where}.${caveat}`,\n };\n}\n\n/** Arm three, split by whether the run was in a position to see the claim fail. */\nfunction undeliveredOutcome(\n claim: Claim,\n census: Map<string, readonly string[]> | undefined,\n caveat: string,\n): ClaimOutcome {\n const { kind, name } = parseRoot(claim.root);\n const blank = { claim, changes: [], subjects: [] } as const;\n\n if (kind === 'shape') {\n return {\n ...blank,\n verdict: 'undelivered',\n // A shape is a digest of a difference. Claiming one that did not occur is\n // always the strong reading: nothing else could have produced it.\n because:\n `declared (${claim.reason}) and no change in this run carries shape ` +\n `\\`${name}\\` — nothing produced that difference.${caveat}`,\n };\n }\n\n if (census === undefined) {\n return {\n ...blank,\n verdict: 'unobservable',\n because:\n `declared (${claim.reason}) and nothing changed under \\`${name}\\`, but this run ` +\n 'kept no component census, so it cannot say whether `' +\n name +\n '` was rendered at all. The claim was not checked.' +\n caveat,\n };\n }\n\n const rendered = census.get(name);\n\n if (rendered === undefined) {\n return {\n ...blank,\n verdict: 'unobservable',\n because:\n `declared (${claim.reason}) and this run never rendered \\`${name}\\` in any subject, ` +\n 'so nothing here is evidence about it either way. Check the subject selection, or ' +\n 'whether provenance names this component.' +\n caveat,\n };\n }\n\n return {\n ...blank,\n verdict: 'undelivered',\n because:\n `declared (${claim.reason}) and \\`${name}\\` rendered in ${rendered.length} subject(s) ` +\n `— ${preview(rendered)} — and did not change. The edit did not take: wrong file, a ` +\n 'dead branch, a rule something else overrides, or a stale build.' +\n caveat,\n };\n}\n\nfunction unclaimedBecause(change: Change): string {\n const named = change.component ?? '(no component resolved; grouped by shape alone)';\n const settle =\n change.settles.length === 0\n ? 'nothing it can settle on its own'\n : `settles ${change.settles.length} of them`;\n\n return (\n `${named} moved and no claim covers it — ${change.subjects.length} subject(s), ` +\n `${change.pixels} pixel(s), ${settle}` +\n (change.cause ? '' : '; it was displaced rather than edited')\n );\n}\n\nfunction worst(\n outcomes: readonly ClaimOutcome[],\n unclaimed: readonly UnclaimedChange[],\n clustering: Clustering,\n): RunAdjudication['verdict'] {\n if (outcomes.some((outcome) => outcome.verdict === 'undelivered')) return 'unmet';\n if (unclaimed.length > 0 || clustering.ungrouped.length > 0) return 'review';\n if (outcomes.some((outcome) => outcome.verdict !== 'delivered')) return 'review';\n return 'clean';\n}\n\n/**\n * The adjudication as the thing an agent reads before deciding what to do next.\n *\n * Ordered by what it should act on, not by what the run computed first:\n * undelivered claims lead, because an agent triaging collateral from an edit that\n * never happened is an agent about to make its second mistake.\n */\nexport function describeAdjudication(result: RunAdjudication): string {\n const counts = tally(result);\n const lines: string[] = [headline(result, counts)];\n\n const order: readonly ClaimVerdict[] = ['undelivered', 'unobservable', 'overreached', 'delivered'];\n for (const verdict of order) {\n for (const outcome of result.claims.filter((claim) => claim.verdict === verdict)) {\n lines.push('', ` [${outcome.verdict}] ${outcome.claim.root}`, ` ${outcome.because}`);\n for (const change of outcome.changes) {\n if (change.file !== undefined) lines.push(` ${change.file}`);\n }\n }\n }\n\n for (const entry of result.unclaimed) {\n lines.push('', ` [unclaimed] ${entry.change.component ?? entry.change.fingerprint}`);\n lines.push(` ${entry.because}`);\n if (entry.change.file !== undefined) lines.push(` ${entry.change.file}`);\n if (entry.change.settles.length > 0) {\n lines.push(` variance accept --shape ${entry.change.fingerprint}`);\n }\n }\n\n if (result.ungrouped.length > 0) {\n lines.push(\n '',\n ` [ungrouped] ${result.ungrouped.length} changed subject(s) carry no shape, so no claim ` +\n 'could be checked against them: ' +\n preview(result.ungrouped),\n );\n }\n\n if (result.blind.length > 0) {\n lines.push(\n '',\n ` [not observed] ${result.blind.length} subject(s) were not looked at, so every line ` +\n 'above is bounded by what this run saw: ' +\n preview(result.blind.map((entry) => entry.subject)),\n );\n }\n\n return lines.join('\\n');\n}\n\nfunction headline(result: RunAdjudication, counts: Record<ClaimVerdict, number>): string {\n const claims =\n `${result.claims.length} claim(s): ${counts.delivered} delivered, ` +\n `${counts.undelivered} undelivered, ${counts.overreached} over-reaching, ` +\n `${counts.unobservable} unchecked.`;\n\n const lead =\n result.verdict === 'unmet'\n ? 'An edit you declared did not take. Fix that before reading anything else.'\n : result.verdict === 'review'\n ? 'Every declared edit landed; something you did not declare also moved.'\n : 'Every declared edit landed, and nothing else moved.';\n\n return `${lead}\\n${claims} ${result.unclaimed.length} unclaimed change(s).`;\n}\n\nfunction tally(result: RunAdjudication): Record<ClaimVerdict, number> {\n const counts: Record<ClaimVerdict, number> = {\n delivered: 0,\n overreached: 0,\n undelivered: 0,\n unobservable: 0,\n };\n for (const outcome of result.claims) counts[outcome.verdict] += 1;\n return counts;\n}\n\n/** The first few, because a reader wants examples and not a manifest. */\nfunction preview(items: readonly string[]): string {\n return items.length <= 4\n ? items.join(', ')\n : `${items.slice(0, 4).join(', ')}, and ${items.length - 4} more`;\n}\n"]}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * What a presentation reading contributes to a regression report.
3
+ *
4
+ * Beside `format.ts` rather than inside it for the reason `finding-record.ts` is:
5
+ * the record types a report carries are one shape each, and a file holding every
6
+ * shape at once is read by nobody looking for one of them.
7
+ */
8
+ import type { Digest } from '@variance-authority/core';
9
+ /** How one relationship condition changed between comparable presentation readings. */
10
+ export type PresentationEffectTransition = 'introduced' | 'resolved' | 'persisted';
11
+ /** The finding-local measurement retained on one side of a presentation effect. */
12
+ export interface PresentationEffectEvidence {
13
+ /** Finding id in the corresponding presentation report or hierarchy reading. */
14
+ readonly finding: string;
15
+ readonly measurements: Readonly<Record<string, number | string>>;
16
+ }
17
+ /** One relationship consequence attributable to the difference between two readings. */
18
+ export interface PresentationEffectRecord {
19
+ readonly rule: string;
20
+ readonly transition: PresentationEffectTransition;
21
+ readonly owner: string;
22
+ readonly nodes: readonly string[];
23
+ readonly pattern?: string;
24
+ readonly contract?: string;
25
+ readonly before?: PresentationEffectEvidence;
26
+ readonly after?: PresentationEffectEvidence;
27
+ }
28
+ /** Information-volume evidence kept beside relationship effects. */
29
+ export interface PresentationInformationRecord {
30
+ readonly contentPreserved: boolean;
31
+ readonly characters: {
32
+ readonly before: number;
33
+ readonly after: number;
34
+ readonly delta: number;
35
+ };
36
+ readonly elements: {
37
+ readonly before: number;
38
+ readonly after: number;
39
+ readonly delta: number;
40
+ };
41
+ readonly repeatedObjects: {
42
+ readonly before: number;
43
+ readonly after: number;
44
+ readonly delta: number;
45
+ };
46
+ }
47
+ /**
48
+ * Presentation evidence carried by a general regression report.
49
+ *
50
+ * `incomparable` has no effects: inventing a transition from one reading would
51
+ * turn absence into evidence. Comparable readings always carry `effects`, where
52
+ * an empty array means the presentation consequence was measured and unchanged.
53
+ */
54
+ export type PresentationSignalRecord = {
55
+ readonly verdict: 'incomparable';
56
+ readonly because: string;
57
+ readonly before?: Digest;
58
+ readonly after?: Digest;
59
+ } | {
60
+ readonly verdict: 'unchanged' | 'changed';
61
+ readonly before: Digest;
62
+ readonly after: Digest;
63
+ readonly information: PresentationInformationRecord;
64
+ readonly effects: readonly PresentationEffectRecord[];
65
+ };
66
+ //# sourceMappingURL=presentation-record.d.ts.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * What a presentation reading contributes to a regression report.
3
+ *
4
+ * Beside `format.ts` rather than inside it for the reason `finding-record.ts` is:
5
+ * the record types a report carries are one shape each, and a file holding every
6
+ * shape at once is read by nobody looking for one of them.
7
+ */
8
+ export {};
9
+ //# sourceMappingURL=presentation-record.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"presentation-record.js","sourceRoot":"","sources":["../src/presentation-record.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG","sourcesContent":["/**\n * What a presentation reading contributes to a regression report.\n *\n * Beside `format.ts` rather than inside it for the reason `finding-record.ts` is:\n * the record types a report carries are one shape each, and a file holding every\n * shape at once is read by nobody looking for one of them.\n */\n\nimport type { Digest } from '@variance-authority/core';\n\n/** How one relationship condition changed between comparable presentation readings. */\nexport type PresentationEffectTransition = 'introduced' | 'resolved' | 'persisted';\n\n/** The finding-local measurement retained on one side of a presentation effect. */\nexport interface PresentationEffectEvidence {\n /** Finding id in the corresponding presentation report or hierarchy reading. */\n readonly finding: string;\n readonly measurements: Readonly<Record<string, number | string>>;\n}\n\n/** One relationship consequence attributable to the difference between two readings. */\nexport interface PresentationEffectRecord {\n readonly rule: string;\n readonly transition: PresentationEffectTransition;\n readonly owner: string;\n readonly nodes: readonly string[];\n readonly pattern?: string;\n readonly contract?: string;\n readonly before?: PresentationEffectEvidence;\n readonly after?: PresentationEffectEvidence;\n}\n\n/** Information-volume evidence kept beside relationship effects. */\nexport interface PresentationInformationRecord {\n readonly contentPreserved: boolean;\n readonly characters: { readonly before: number; readonly after: number; readonly delta: number };\n readonly elements: { readonly before: number; readonly after: number; readonly delta: number };\n readonly repeatedObjects: { readonly before: number; readonly after: number; readonly delta: number };\n}\n\n/**\n * Presentation evidence carried by a general regression report.\n *\n * `incomparable` has no effects: inventing a transition from one reading would\n * turn absence into evidence. Comparable readings always carry `effects`, where\n * an empty array means the presentation consequence was measured and unchanged.\n */\nexport type PresentationSignalRecord =\n | {\n readonly verdict: 'incomparable';\n readonly because: string;\n readonly before?: Digest;\n readonly after?: Digest;\n }\n | {\n readonly verdict: 'unchanged' | 'changed';\n readonly before: Digest;\n readonly after: Digest;\n readonly information: PresentationInformationRecord;\n readonly effects: readonly PresentationEffectRecord[];\n };\n\n"]}