create-pathfinder 4.1.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/outcome.mjs CHANGED
@@ -38,11 +38,15 @@
38
38
  * @param {{written: number, skipped: number, overwritten: number,
39
39
  * errors: {relativePath: string, message: string}[]}} args.result
40
40
  * @param {{plan: object[], result: object, blocked: boolean}} args.adapters
41
+ * @param {{plan: object[], result: object, blocked: boolean}} [args.hooks]
42
+ * the session hook handler plan and result, shaped exactly like `adapters`.
43
+ * Optional, and absent means "no handler was planned" — a harness with no
44
+ * lifecycle event has none, which is the ordinary case rather than an error.
41
45
  * @param {{label: string}[]} args.harnesses the selected harnesses, registry order
42
46
  * @param {{dryRun?: boolean}} args.options
43
47
  * @returns {Readonly<object>} frozen; rows and lists frozen with it
44
48
  */
45
- export function summarize({ plan, result, adapters, harnesses, options }) {
49
+ export function summarize({ plan, result, adapters, hooks = NO_HOOKS, harnesses, options }) {
46
50
  // A dry run has no `result.written` to report, because nothing was written.
47
51
  // The plan is counted instead, which is the same number the run would have
48
52
  // produced had it been allowed to write.
@@ -54,9 +58,14 @@ export function summarize({ plan, result, adapters, harnesses, options }) {
54
58
  .filter((item) => item.status === "skip")
55
59
  .map((item) => item.relativePath);
56
60
 
57
- // Copy errors before adapter errors, because that is the order they happened
58
- // in and the order the failure list has always printed.
59
- const failures = Object.freeze([...result.errors, ...adapters.result.errors]);
61
+ // Copy errors before adapter errors before handler errors, because that is
62
+ // the order they happened in and the order the failure list has always
63
+ // printed.
64
+ const failures = Object.freeze([
65
+ ...result.errors,
66
+ ...adapters.result.errors,
67
+ ...hooks.result.errors,
68
+ ]);
60
69
 
61
70
  return Object.freeze({
62
71
  written,
@@ -71,11 +80,35 @@ export function summarize({ plan, result, adapters, harnesses, options }) {
71
80
  // that errored and this does not, so the two disagree exactly when a write
72
81
  // fails — and this is the number the closing headline speaks for.
73
82
  built: adapters.result.generated + adapters.result.replaced,
74
- attention: attentionCount(adapters),
75
- harnessRows: harnessRows({ adapters, harnesses }),
83
+ // Counted apart from `built`, and never folded into it. A handler is not
84
+ // an adapter it delegates to nothing and is inert until a human
85
+ // activates it — so adding one to the adapter count would make the closing
86
+ // line state a number that is not true.
87
+ handlers: hooks.result.generated + hooks.result.replaced,
88
+ attention: attentionCount(adapters) + attentionCount(hooks),
89
+ harnessRows: harnessRows({ adapters, hooks, harnesses }),
76
90
  });
77
91
  }
78
92
 
93
+ /**
94
+ * What a run with no hook plan looks like.
95
+ *
96
+ * The same shape `generateHooks` returns, so a caller that has none and a
97
+ * caller whose harnesses have none reach identical code below.
98
+ */
99
+ const NO_HOOKS = Object.freeze({
100
+ plan: Object.freeze([]),
101
+ result: Object.freeze({
102
+ generated: 0,
103
+ replaced: 0,
104
+ unchanged: 0,
105
+ conflicts: Object.freeze([]),
106
+ orphans: Object.freeze([]),
107
+ errors: Object.freeze([]),
108
+ }),
109
+ blocked: false,
110
+ });
111
+
79
112
  /**
80
113
  * What actually wants a human: a contested path, or an adapter pointing at a
81
114
  * skill that is gone.
@@ -89,9 +122,9 @@ export function summarize({ plan, result, adapters, harnesses, options }) {
89
122
  * errored paths included, because a path that could not be written is still a
90
123
  * path somebody has to go and look at.
91
124
  */
92
- function attentionCount(adapters) {
93
- if (adapters.blocked) return 0;
94
- return adapters.plan.filter(
125
+ function attentionCount(generated) {
126
+ if (generated.blocked) return 0;
127
+ return generated.plan.filter(
95
128
  (item) => item.action === "conflict" || item.action === "orphan",
96
129
  ).length;
97
130
  }
@@ -110,38 +143,57 @@ function attentionCount(adapters) {
110
143
  * empty plan, so that a harness which was chosen and never reached is never
111
144
  * described as having generated zero adapters.
112
145
  *
113
- * Paths that failed to write are excluded from every count and list here: an
114
- * adapter that could not be written was not generated, is not up to date, and
115
- * is not a conflict the user can resolve by re-running with `--force`. They are
116
- * reported once, as failures.
146
+ * Each row carries a harness's adapters and, under `handlers`, that harness's
147
+ * hook handlers the same five facts twice, because they are the same five
148
+ * questions about two different kinds of generated file.
117
149
  */
118
- function harnessRows({ adapters, harnesses }) {
150
+ function harnessRows({ adapters, hooks, harnesses }) {
119
151
  if (harnesses.length === 0 || adapters.blocked) return Object.freeze([]);
120
152
 
121
- const failed = new Set(adapters.result.errors.map((error) => error.relativePath));
122
-
123
153
  return Object.freeze(
124
- harnesses.map((harness) => {
125
- // Identity, not label: the harness object on a plan item is the registry
126
- // entry itself, and two entries could plausibly share a label one day.
127
- const mine = adapters.plan.filter(
154
+ harnesses.map((harness) =>
155
+ Object.freeze({
156
+ harness,
157
+ ...tally(adapters, harness),
158
+ // The same five facts for this harness's hook handlers, under their
159
+ // own names. A harness with no lifecycle event has no handlers, and
160
+ // every one of these is then zero or empty — which is what a report
161
+ // needs in order to say nothing at all about them.
162
+ handlers: Object.freeze(tally(hooks, harness)),
163
+ }),
164
+ ),
165
+ );
166
+ }
167
+
168
+ /**
169
+ * One harness's share of a generated plan, as the five facts a report prints.
170
+ *
171
+ * Paths that failed to write are excluded from every count and list: a file
172
+ * that could not be written was not generated, is not up to date, and is not a
173
+ * conflict the user can resolve by re-running with `--force`. They are
174
+ * reported once, as failures.
175
+ */
176
+ function tally(generated, harness) {
177
+ const failed = new Set(generated.result.errors.map((error) => error.relativePath));
178
+
179
+ // Identity, not label: the harness object on a plan item is the registry
180
+ // entry itself, and two entries could plausibly share a label one day.
181
+ const mine = generated.blocked
182
+ ? []
183
+ : generated.plan.filter(
128
184
  (item) => item.harness === harness && !failed.has(item.relativePath),
129
185
  );
130
- const count = (action) => mine.filter((item) => item.action === action).length;
131
- const paths = (action) =>
132
- Object.freeze(
133
- mine.filter((item) => item.action === action).map((item) => item.relativePath),
134
- );
135
186
 
136
- return Object.freeze({
137
- harness,
138
- generated: count("write"),
139
- replaced: count("replace"),
140
- unchanged: count("up-to-date"),
141
- // `planAdapters` order, which is the order they will be printed in.
142
- conflicts: paths("conflict"),
143
- orphans: paths("orphan"),
144
- });
145
- }),
146
- );
187
+ const count = (action) => mine.filter((item) => item.action === action).length;
188
+ const paths = (action) =>
189
+ Object.freeze(mine.filter((item) => item.action === action).map((item) => item.relativePath));
190
+
191
+ return {
192
+ generated: count("write"),
193
+ replaced: count("replace"),
194
+ unchanged: count("up-to-date"),
195
+ // Plan order, which is the order they will be printed in.
196
+ conflicts: paths("conflict"),
197
+ orphans: paths("orphan"),
198
+ };
147
199
  }