@starci/hfs 5.0.7 → 5.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 (204) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +7 -8
  3. package/cache/fake-session.mjs +1 -1
  4. package/cache/hfs-family.mjs +22 -9
  5. package/cache/import-graph.d.mts +32 -4
  6. package/cache/import-graph.mjs +88 -11
  7. package/cache/lint-family.d.mts +10 -3
  8. package/cache/lint-family.mjs +65 -37
  9. package/cache/records.d.mts +7 -0
  10. package/cache/records.mjs +17 -1
  11. package/cache/run.d.mts +1 -1
  12. package/cache/session.mjs +6 -2
  13. package/cache/side-key.d.mts +2 -0
  14. package/cache/side-key.mjs +5 -1
  15. package/cache/structure-memo.d.mts +34 -0
  16. package/cache/structure-memo.mjs +150 -0
  17. package/cache/types.d.mts +6 -2
  18. package/cache/wiring-family.mjs +1 -1
  19. package/codemod/directive-first.d.mts +48 -0
  20. package/codemod/directive-first.mjs +136 -0
  21. package/codemod/explicit-surface-edit.d.mts +14 -0
  22. package/codemod/explicit-surface-edit.mjs +57 -0
  23. package/codemod/explicit-surface-names.d.mts +30 -0
  24. package/codemod/explicit-surface-names.mjs +135 -0
  25. package/codemod/explicit-surface-render.d.mts +29 -0
  26. package/codemod/explicit-surface-render.mjs +197 -0
  27. package/codemod/explicit-surface.d.mts +50 -0
  28. package/codemod/explicit-surface.mjs +374 -0
  29. package/codemod/main.d.mts +7 -5
  30. package/codemod/main.mjs +37 -16
  31. package/codemod/small-renames.mjs +22 -2
  32. package/codemod/source-tree.d.mts +8 -0
  33. package/codemod/source-tree.mjs +23 -0
  34. package/lint/eslint-list.cjs +55 -0
  35. package/lint/run.d.mts +35 -61
  36. package/lint/run.mjs +91 -288
  37. package/package.json +1 -1
  38. package/release/lint-report.d.mts +1 -1
  39. package/release/lint-report.mjs +3 -3
  40. package/report/sonar.d.mts +1 -1
  41. package/report/sonar.mjs +3 -3
  42. package/report/stylelint-findings.d.mts +23 -0
  43. package/report/stylelint-findings.mjs +9 -0
  44. package/runtime/knowledge/hfs/canon-pins.yaml +8 -8
  45. package/runtime/knowledge/hfs/ci-standard.yaml +2 -2
  46. package/runtime/knowledge/hfs/rules.yaml +56 -5
  47. package/runtime/knowledge/hfs/slots.yaml +1 -1
  48. package/runtime/knowledge/sonar-gate.yaml +2 -2
  49. package/runtime/modules/kernel/failure-codes.yaml +4 -4
  50. package/runtime/scripts/api/node/run-node.d.mts +5 -0
  51. package/runtime/scripts/hfs/architecture/test-world-files.d.mts +4 -3
  52. package/runtime/scripts/hfs/architecture/test-world-files.mjs +26 -7
  53. package/runtime/scripts/hfs/check.d.mts +16 -1
  54. package/runtime/scripts/hfs/check.mjs +18 -7
  55. package/runtime/scripts/hfs/explicit-surface-rule.d.mts +58 -0
  56. package/runtime/scripts/hfs/explicit-surface-rule.mjs +59 -0
  57. package/runtime/scripts/hfs/gate-threshold.mjs +1 -1
  58. package/runtime/scripts/hfs/rule-memo.d.mts +30 -0
  59. package/runtime/scripts/hfs/rule-memo.mjs +24 -0
  60. package/runtime/scripts/hfs/rules/a15-findings.d.mts +6 -0
  61. package/runtime/scripts/hfs/rules/a15-findings.mjs +11 -2
  62. package/runtime/scripts/hfs/rules/ci-standard.mjs +2 -2
  63. package/runtime/scripts/hfs/rules/monorepo.d.mts +1 -3
  64. package/runtime/scripts/hfs/rules/monorepo.mjs +9 -11
  65. package/runtime/scripts/hfs/rules/pipeline.mjs +13 -9
  66. package/runtime/scripts/hfs/rules/read.d.mts +5 -0
  67. package/runtime/scripts/hfs/rules/read.mjs +13 -0
  68. package/runtime/scripts/hfs/rules/secrets.d.mts +2 -0
  69. package/runtime/scripts/hfs/rules/secrets.mjs +4 -1
  70. package/runtime/scripts/hfs/rules/stacks-release.mjs +35 -21
  71. package/runtime/scripts/lib/declaration-surface.d.mts +35 -0
  72. package/runtime/scripts/lib/declaration-surface.mjs +112 -0
  73. package/runtime/scripts/lib/worktree-exclude.mjs +1 -1
  74. package/scaffold/app.mjs +3 -3
  75. package/src/check-cache-structure-support.d.mts +52 -0
  76. package/src/main.d.mts +3 -2
  77. package/src/main.mjs +123 -109
  78. package/sync/ci-commands.d.mts +2 -1
  79. package/sync/ci-commands.mjs +19 -20
  80. package/sync/index.mjs +2 -2
  81. package/templates/app/package-scripts/core.json +2 -2
  82. package/templates/app/task-graph/turbo.json +0 -3
  83. package/templates/be/inbox/delivery-recorded.event.ts.tpl +2 -2
  84. package/templates/be/inbox/entities.index.ts.tpl +1 -1
  85. package/templates/be/inbox/module-definition.ts.tpl +8 -2
  86. package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +10 -2
  87. package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +8 -2
  88. package/templates/be/patterns/api/feature.module-definition.ts.tpl +9 -3
  89. package/templates/be/patterns/api/graphql.module-definition.ts.tpl +9 -3
  90. package/templates/be/patterns/cli/group.module-definition.ts.tpl +9 -3
  91. package/templates/be/patterns/event-bus/platform/event-bus.module-definition.ts.tpl +8 -3
  92. package/templates/be/patterns/event-bus/platform/persistence/connection.ts.tpl +3 -2
  93. package/templates/be/patterns/event-bus/platform/persistence/event-bus.sql.ts.tpl +4 -3
  94. package/templates/be/patterns/idempotency/platform/inbox/inbox.module-definition.ts.tpl +8 -3
  95. package/templates/be/patterns/idempotency/platform/inbox/persistence/connection.ts.tpl +3 -2
  96. package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.sql.ts.tpl +7 -6
  97. package/templates/be/patterns/jobs/job.module-definition.ts.tpl +9 -3
  98. package/templates/be/patterns/jobs/job.processor.ts.tpl +1 -1
  99. package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +9 -3
  100. package/templates/be/patterns/jobs/platform/jobs.module-definition.ts.tpl +8 -3
  101. package/templates/be/patterns/jobs/platform/persistence.connection.ts.tpl +3 -2
  102. package/templates/be/patterns/jobs/platform/persistence.jobs.sql.ts.tpl +5 -4
  103. package/templates/be/patterns/projections/persistence.connection.ts.tpl +3 -2
  104. package/templates/be/patterns/projections/projection.module-definition.ts.tpl +8 -3
  105. package/templates/be/patterns/queues/platform/persistence.connection.ts.tpl +3 -2
  106. package/templates/be/patterns/queues/platform/persistence.queue.sql.ts.tpl +4 -3
  107. package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +8 -3
  108. package/templates/be/patterns/queues/queue.module-definition.ts.tpl +9 -3
  109. package/templates/be/patterns/reactors/consumer.ts.tpl +1 -1
  110. package/templates/be/patterns/reactors/message.module-definition.ts.tpl +9 -3
  111. package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +9 -3
  112. package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +9 -3
  113. package/templates/be/patterns/realtime/channel.sse.module-definition.ts.tpl +9 -3
  114. package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +9 -3
  115. package/templates/be/patterns/saga/saga.done.consumer.ts.tpl +1 -1
  116. package/templates/be/patterns/saga/saga.failed.consumer.ts.tpl +1 -1
  117. package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +9 -3
  118. package/templates/be/patterns/saga/saga.module-definition.ts.tpl +9 -3
  119. package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +9 -3
  120. package/templates/be/platform/graphql/graphql.module-definition.ts +8 -3
  121. package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +9 -3
  122. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +9 -3
  123. package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +9 -3
  124. package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +9 -3
  125. package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +9 -3
  126. package/templates/be/skeleton/src/modules/domain/identity/identity.module-definition.ts +8 -3
  127. package/templates/be/skeleton/src/modules/domain/liveness/liveness.module-definition.ts +8 -3
  128. package/templates/be/skeleton/src/modules/domain/note/note.module-definition.ts +8 -3
  129. package/templates/be/skeleton/src/modules/domain/note/persistence/connection.ts +3 -2
  130. package/templates/be/skeleton/src/modules/domain/note/persistence/note.sql.ts +2 -1
  131. package/templates/be/skeleton/src/modules/platform/clock/clock.module-definition.ts +8 -3
  132. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.module-definition.ts +8 -3
  133. package/templates/be/skeleton/src/modules/platform/database/database.module-definition.ts +8 -3
  134. package/templates/be/skeleton/src/modules/platform/database/index.ts +1 -0
  135. package/templates/be/skeleton/src/modules/platform/errors/errors.module-definition.ts +10 -4
  136. package/templates/be/skeleton/src/modules/platform/http/http.module-definition.ts +8 -4
  137. package/templates/be/skeleton/src/modules/platform/http-security/http-security.module-definition.ts +10 -4
  138. package/templates/be/skeleton/src/modules/platform/http-security/persistence/connection.ts +3 -2
  139. package/templates/be/skeleton/src/modules/platform/http-security/persistence/rate-limit.sql.ts +3 -2
  140. package/templates/be/skeleton/src/modules/platform/i18n/i18n.module-definition.ts +10 -4
  141. package/templates/be/skeleton/src/modules/platform/ids/ids.module-definition.ts +8 -3
  142. package/templates/be/skeleton/src/modules/platform/logging/logging.module-definition.ts +8 -3
  143. package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +8 -3
  144. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +1 -1
  145. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/google.strategy.ts +5 -1
  146. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/local.strategy.ts +5 -1
  147. package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +1 -1
  148. package/templates/be/skeleton-lite/src/modules/platform/http-security/persistence/entities/index.ts +1 -1
  149. package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +7 -1
  150. package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +7 -1
  151. package/templates/be/table/api/feature.module-definition.ts.tpl +9 -3
  152. package/templates/be/table/api/graphql.module-definition.ts.tpl +9 -3
  153. package/templates/be/table/entities.index.ts.tpl +1 -1
  154. package/templates/be/table/module-definition.ts.tpl +8 -3
  155. package/templates/be/test-world/fixtures/migrations.sql.ts.tpl +1 -0
  156. package/templates/be/test-world/world/test-world.config.ts.tpl +8 -1
  157. package/templates/fe/common/app-layout.tsx.partial +1 -1
  158. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +2 -1
  159. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignIn.ts +11 -3
  160. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +8 -2
  161. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/store.ts +2 -1
  162. package/templates/fe/skeleton/apps/__app__/src/modules/toast/hooks.ts +1 -1
  163. package/templates/fe/skeleton/apps/__app__/src/modules/toast/toast.tsx +4 -1
  164. package/templates/fe/skeleton/apps/__app__/src/redux/hooks.ts +3 -2
  165. package/templates/fe/skeleton/apps/__app__/src/redux/index.ts +1 -0
  166. package/templates/fe/skeleton/apps/__app__/src/redux/slices/session.ts +12 -4
  167. package/templates/fe/skeleton/apps/__app__/src/redux/store.ts +8 -3
  168. package/templates/fe/skeleton/apps/app/src/app/[locale]/layout.tsx +4 -3
  169. package/templates/fe/skeleton/apps/app/src/features/layouts/AppLayout/index.tsx +1 -1
  170. package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +2 -1
  171. package/templates/fe/skeleton/apps/app/src/features/pages/AppLoadingPage/index.tsx +2 -1
  172. package/templates/fe/skeleton/apps/app/src/features/pages/AppNotFoundPage/index.tsx +2 -1
  173. package/templates/fe/skeleton/apps/app/src/modules/i18n/index.ts +2 -1
  174. package/templates/fe/skeleton/apps/app/src/modules/i18n/request.ts +4 -1
  175. package/templates/fe/skeleton/apps/landing/src/app/[locale]/layout.tsx +4 -3
  176. package/templates/fe/skeleton/apps/landing/src/features/layouts/LandingLayout/index.tsx +2 -2
  177. package/templates/fe/skeleton/apps/landing/src/features/pages/LandingLoadingPage/index.tsx +2 -1
  178. package/templates/fe/skeleton/apps/landing/src/features/pages/LandingNotFoundPage/index.tsx +2 -1
  179. package/templates/fe/skeleton/apps/landing/src/features/pages/LandingPage/index.tsx +2 -1
  180. package/templates/fe/skeleton/apps/landing/src/modules/i18n/index.ts +2 -1
  181. package/templates/fe/skeleton/apps/landing/src/modules/i18n/request.ts +4 -1
  182. package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +19 -1
  183. package/templates/fe/skeleton/packages/__project__-i18n/src/proxy.ts +1 -1
  184. package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +5 -1
  185. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +4 -3
  186. package/templates/fe/skeleton-lite/apps/web/src/features/layouts/AppLayout/index.tsx +2 -2
  187. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +2 -1
  188. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppLoadingPage/index.tsx +2 -1
  189. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppNotFoundPage/index.tsx +2 -1
  190. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +3 -1
  191. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +5 -1
  192. package/templates/fe/table/graphql/useMutateSwr.ts.tpl +2 -1
  193. package/templates/fe/table/graphql/useQuerySwr.ts.tpl +2 -1
  194. package/templates/fe/transport/graphql/clients/clients/auth.ts +1 -1
  195. package/templates/fe/transport/graphql/clients/clients/no-auth.ts +1 -1
  196. package/templates/fe/transport/graphql/clients/links/attach-access-token.ts +1 -1
  197. package/templates/fe/transport/graphql/clients/links/error.ts +1 -1
  198. package/templates/fe/transport/graphql/clients/links/http.ts +1 -1
  199. package/templates/fe/transport/graphql/clients/links/locale.ts +1 -1
  200. package/templates/fe/transport/graphql/clients/links/refresh-token.ts +1 -1
  201. package/templates/fe/transport/graphql/clients/links/retry.ts +1 -1
  202. package/templates/fe/transport/graphql/clients/links/timeout.ts +1 -1
  203. package/templates/fe/units/page/index.tsx.tpl +3 -2
  204. package/templates/fe/units/slot/SlotView.tsx.tpl +1 -1
@@ -138,17 +138,46 @@ const isFile = (file) => {
138
138
  return false;
139
139
  }
140
140
  };
141
+ const isDirectory = (folder) => {
142
+ try {
143
+ return fs.statSync(folder).isDirectory();
144
+ }
145
+ catch {
146
+ return false;
147
+ }
148
+ };
149
+ /**
150
+ * What the import graph reads of the disk, remembered for the build: a side's thousands of files probe the same few hundred folders and
151
+ * candidate paths while resolving their imports, and the tree does not change while the graph is built.
152
+ */
153
+ function graphHost(ts, sideRoot, hashOf) {
154
+ const files = new Map(), folders = new Map();
155
+ const remembered = (known, ask) => (path) => {
156
+ let answer = known.get(path);
157
+ if (answer === undefined) {
158
+ answer = ask(path);
159
+ known.set(path, answer);
160
+ }
161
+ return answer;
162
+ };
163
+ return { ts, sideRoot, readText, fileExists: remembered(files, isFile), directoryExists: remembered(folders, isDirectory), hashOf };
164
+ }
141
165
  /** A record's `data` object, or an empty one. */
142
166
  const dataOf = (record) => (record.data !== null && typeof record.data === 'object' && !Array.isArray(record.data) ? record.data : {});
143
- /** The graph findings of every canon that publishes graph rules, merged by file and sorted. */
144
- function mergedSnapshot(sideRoot, canons) {
167
+ /**
168
+ * The graph findings of every canon that publishes graph rules, merged by file and sorted. A canon whose snapshot the ESLint child handed out (`handed`, by
169
+ * canon name) is not asked to build its graph again; any other canon builds it through its `projectGraph`.
170
+ */
171
+ export function mergedSnapshot(sideRoot, canons, handed) {
145
172
  const merged = {};
146
173
  for (const canon of canons) {
147
174
  if (!canon.graphRules)
148
175
  continue;
149
- if (canon.projectGraph === null)
176
+ const given = handed?.[canon.name];
177
+ if (given === undefined && canon.projectGraph === null)
150
178
  return null;
151
- for (const [file, findings] of Object.entries(canon.projectGraph({ repoRoot: sideRoot }).byFile))
179
+ const { byFile } = given ?? canon.projectGraph({ repoRoot: sideRoot });
180
+ for (const [file, findings] of Object.entries(byFile))
152
181
  merged[posix(file)] = [...(merged[posix(file)] ?? []), ...findings];
153
182
  }
154
183
  for (const list of Object.values(merged))
@@ -162,7 +191,6 @@ const graphRecordFindings = (side, file, findings) => sortFindings(findings.map(
162
191
  */
163
192
  export async function lintSide(input) {
164
193
  const { session, side, sideRoot, scope, stylesheets } = input;
165
- const inReport = input.report ?? (() => true);
166
194
  const scopeSet = new Set(scope);
167
195
  const styleSet = new Set(stylesheets);
168
196
  const folder = `lint/${side}`;
@@ -189,7 +217,7 @@ export async function lintSide(input) {
189
217
  const validStyle = new Map(reconcile(ofStep(LINT_STEPS.stylelint), hashOf).map((record) => [record.path, record]));
190
218
  const meta = ofStep(LINT_STEPS.meta).find((record) => record.path === SIDE_RECORD);
191
219
  const metaData = meta === undefined ? {} : dataOf(meta);
192
- const previousFindings = new Set([...ofStep(LINT_STEPS.eslint), ...ofStep(LINT_STEPS.stylelint)].filter((record) => inReport(record.path)).flatMap((record) => record.findings.map(keyOf)));
220
+ const previousFindings = new Set([...ofStep(LINT_STEPS.eslint), ...ofStep(LINT_STEPS.stylelint)].flatMap((record) => record.findings.map(keyOf)));
193
221
  const progress = session.progress.step('lint', `lint ${side}`, scope.length + stylesheets.length);
194
222
  // ------------------------------------------------------------------ the plan
195
223
  let whole = input.full;
@@ -242,10 +270,10 @@ export async function lintSide(input) {
242
270
  const imports = Array.isArray(data['imports']) ? data['imports'].filter((value) => typeof value === 'string') : [];
243
271
  const kept = previous.get(record.path);
244
272
  if (kept === undefined || record.hash === hashOf(record.path))
245
- previous.set(record.path, { hash: record.hash, imports, declaresGlobals: data['globals'] === true });
273
+ previous.set(record.path, { hash: record.hash, imports, declaresGlobals: data['globals'] === true, api: typeof data['api'] === 'string' ? data['api'] : undefined, open: typeof data['open'] === 'boolean' ? data['open'] : undefined, refs: Array.isArray(data['refs']) ? data['refs'].filter((value) => typeof value === 'string') : undefined });
246
274
  }
247
275
  const recordedGraph = previous.size === 0 ? null : previous;
248
- build = buildImportGraph({ ts, sideRoot, readText, fileExists: isFile, hashOf: (file) => hashOf(file) ?? null }, scope, recordedGraph, { pathsChanged: metaData['paths'] !== pathsDigest });
276
+ build = buildImportGraph(graphHost(ts, sideRoot, (file) => hashOf(file) ?? null), scope, recordedGraph, { pathsChanged: metaData['paths'] !== pathsDigest });
249
277
  const dirty = dirtySetOf(recordedGraph, build.graph);
250
278
  if (dirty.wholeSide !== null)
251
279
  whole = dirty.wholeSide;
@@ -306,14 +334,16 @@ export async function lintSide(input) {
306
334
  dirtyStyle = new Set(stylesheets);
307
335
  }
308
336
  // ------------------------------------------------------------------ the run
309
- const eslintList = [...dirtyEslint].filter(inReport).sort();
310
- const styleList = [...dirtyStyle].filter(inReport).sort();
337
+ const eslintList = [...dirtyEslint].sort();
338
+ const styleList = [...dirtyStyle].sort();
339
+ let handedGraph;
311
340
  const freshEslint = new Map();
312
341
  const freshStyle = new Map();
313
342
  const rootOf = (file) => path.relative(sideRoot, file).replace(/\\/g, '/');
314
343
  if (eslintList.length > 0) {
315
344
  const run = await input.eslint(whole !== null ? null : eslintList);
316
345
  errors.push(...run.errors);
346
+ handedGraph = run.graph;
317
347
  for (const result of run.results)
318
348
  freshEslint.set(posix(rootOf(result.filePath)), result);
319
349
  }
@@ -331,8 +361,6 @@ export async function lintSide(input) {
331
361
  const styleRecords = [];
332
362
  const planHash = (file) => hashOf(file) ?? 'absent';
333
363
  for (const file of scope) {
334
- if (!inReport(file))
335
- continue;
336
364
  const fresh = freshEslint.get(file);
337
365
  const reuse = fresh === undefined ? validEslint.get(file) : undefined;
338
366
  if (fresh !== undefined) {
@@ -349,8 +377,6 @@ export async function lintSide(input) {
349
377
  }
350
378
  }
351
379
  for (const file of stylesheets) {
352
- if (!inReport(file))
353
- continue;
354
380
  const fresh = freshStyle.get(file);
355
381
  const reuse = fresh === undefined ? validStyle.get(file) : undefined;
356
382
  if (fresh !== undefined) {
@@ -367,38 +393,40 @@ export async function lintSide(input) {
367
393
  }
368
394
  }
369
395
  const checked = freshEslint.size + freshStyle.size;
370
- const units = [...scope, ...stylesheets].filter(inReport).length;
396
+ const units = scope.length + stylesheets.length;
371
397
  const reused = Math.max(0, units - checked);
372
398
  progress.tick(units, reused, checked);
373
399
  progress.end();
374
400
  // ------------------------------------------------------------------ the records
375
401
  let written = false;
376
- const complete = errors.length === 0 && input.write && input.report === null && !sideUnchanged;
402
+ const complete = errors.length === 0 && input.write && !sideUnchanged;
377
403
  if (complete) {
378
- const stable = [...freshEslint.keys(), ...freshStyle.keys()].every((file) => hashFile(abs(file)) === hashOf(file));
379
- if (stable) {
380
- const records = [...eslintRecords, ...styleRecords];
381
- // without an import graph or a project graph the next run lints the side whole; the results themselves are still stored
382
- try {
383
- if (build === null) {
384
- const ts = input.typescript();
385
- build = buildImportGraph({ ts, sideRoot, readText, fileExists: isFile, hashOf: (file) => hashOf(file) ?? null }, scope, null);
386
- }
387
- for (const [file, node] of build.graph)
388
- records.push({ path: file, step: LINT_STEPS.imports, hash: node.hash, findings: [], data: { globals: node.declaresGlobals, imports: node.imports } });
389
- snapshot ??= mergedSnapshot(sideRoot, input.canons);
390
- for (const [file, findings] of Object.entries(snapshot ?? {})) {
391
- if (scopeSet.has(file) && findings.length > 0)
392
- records.push({ path: file, step: LINT_STEPS.graph, hash: planHash(file), findings: graphRecordFindings(side, file, findings) });
393
- }
404
+ // A file edited while the run read it gets no record of any step, and the others keep theirs: their hashes are those of the tree the run
405
+ // planned from, so the edited file is then added (no recorded graph entry) and its dependents are judged again, whatever they read of it.
406
+ const moved = new Set([...freshEslint.keys(), ...freshStyle.keys()].filter((file) => hashFile(abs(file)) !== hashOf(file)));
407
+ const records = [...eslintRecords, ...styleRecords].filter((record) => !moved.has(record.path));
408
+ // without an import graph or a project graph the next run lints the side whole; the results themselves are still stored
409
+ try {
410
+ if (build === null) {
411
+ const ts = input.typescript();
412
+ build = buildImportGraph(graphHost(ts, sideRoot, (file) => hashOf(file) ?? null), scope, null);
394
413
  }
395
- catch {
396
- records.splice(0, records.length, ...records.filter((record) => record.step === LINT_STEPS.eslint || record.step === LINT_STEPS.stylelint));
414
+ for (const [file, node] of build.graph) {
415
+ if (!moved.has(file))
416
+ records.push({ path: file, step: LINT_STEPS.imports, hash: node.hash, findings: [], data: { globals: node.declaresGlobals, imports: node.imports, ...(node.api === undefined ? {} : { api: node.api }), open: node.open !== false, ...(node.refs === undefined ? {} : { refs: node.refs }) } });
397
417
  }
398
- records.push({ path: SIDE_RECORD, step: LINT_STEPS.meta, hash: sideKey, findings: [], data: { extra: input.extraKey, files: scope.length, paths: pathsDigest, stylesheets: stylesheets.length } });
399
- session.replace(folder, records);
400
- written = true;
418
+ snapshot ??= mergedSnapshot(sideRoot, input.canons, handedGraph);
419
+ for (const [file, findings] of Object.entries(snapshot ?? {})) {
420
+ if (scopeSet.has(file) && !moved.has(file) && findings.length > 0)
421
+ records.push({ path: file, step: LINT_STEPS.graph, hash: planHash(file), findings: graphRecordFindings(side, file, findings) });
422
+ }
423
+ }
424
+ catch {
425
+ records.splice(0, records.length, ...records.filter((record) => record.step === LINT_STEPS.eslint || record.step === LINT_STEPS.stylelint));
401
426
  }
427
+ records.push({ path: SIDE_RECORD, step: LINT_STEPS.meta, hash: sideKey, findings: [], data: { extra: input.extraKey, files: scope.length, paths: pathsDigest, stylesheets: stylesheets.length } });
428
+ session.replace(folder, records);
429
+ written = true;
402
430
  }
403
431
  const currentKeys = new Set(current.map(keyOf));
404
432
  let knownFindings = 0;
@@ -4,9 +4,15 @@ export declare class RecordSet {
4
4
  constructor(initial?: readonly CacheRecord[]);
5
5
  /** The record of (path, step) whose hash is `hash`, else undefined: a record of other content is no answer. */
6
6
  matching(path: string, step: string, hash: string): CacheRecord | undefined;
7
+ /** The record of (path, step) whatever its hash, for a step that validates the record's own inputs. */
8
+ get(path: string, step: string): CacheRecord | undefined;
7
9
  put(record: CacheRecord): void;
8
10
  /** Drops every record of `step` whose path is not in `keep`: a deleted path drops every record it owns. */
9
11
  retain(step: string, keep: ReadonlySet<string>): void;
12
+ /** Drops every record whose step starts with `prefix` and whose path is not in `keep`. */
13
+ retainPrefix(prefix: string, keep: ReadonlySet<string>): void;
14
+ /** Drops every record whose step starts with `prefix` and is not one of `keep`: a unit step no rule asked for this run belongs to no rule any more. */
15
+ dropPrefixExcept(prefix: string, keep: ReadonlySet<string>): void;
10
16
  /** Every finding the records of `step` hold. */
11
17
  findings(step: string): Finding[];
12
18
  toArray(): CacheRecord[];
@@ -24,6 +30,7 @@ export declare function scopedStep<T extends JsonValue>(session: CheckSession, r
24
30
  readonly key: string;
25
31
  readonly reuse: boolean;
26
32
  readonly inputs: readonly string[];
33
+ readonly captured?: Readonly<Record<string, string>>;
27
34
  compute(): T | Promise<T>;
28
35
  compact(value: T): Finding[];
29
36
  store?(value: T): boolean;
package/cache/records.mjs CHANGED
@@ -14,6 +14,10 @@ export class RecordSet {
14
14
  const record = this.records.get(keyOf(path, step));
15
15
  return record?.hash === hash ? record : undefined;
16
16
  }
17
+ /** The record of (path, step) whatever its hash, for a step that validates the record's own inputs. */
18
+ get(path, step) {
19
+ return this.records.get(keyOf(path, step));
20
+ }
17
21
  put(record) {
18
22
  this.records.set(keyOf(record.path, record.step), record);
19
23
  }
@@ -23,6 +27,18 @@ export class RecordSet {
23
27
  if (record.step === step && !keep.has(record.path))
24
28
  this.records.delete(key);
25
29
  }
30
+ /** Drops every record whose step starts with `prefix` and whose path is not in `keep`. */
31
+ retainPrefix(prefix, keep) {
32
+ for (const [key, record] of this.records)
33
+ if (record.step.startsWith(prefix) && !keep.has(record.path))
34
+ this.records.delete(key);
35
+ }
36
+ /** Drops every record whose step starts with `prefix` and is not one of `keep`: a unit step no rule asked for this run belongs to no rule any more. */
37
+ dropPrefixExcept(prefix, keep) {
38
+ for (const [key, record] of this.records)
39
+ if (record.step.startsWith(prefix) && !keep.has(record.step))
40
+ this.records.delete(key);
41
+ }
26
42
  /** Every finding the records of `step` hold. */
27
43
  findings(step) {
28
44
  return [...this.records.values()].filter((record) => record.step === step).flatMap((record) => record.findings);
@@ -48,7 +64,7 @@ export async function scopedStep(session, records, step) {
48
64
  if (held?.data !== undefined)
49
65
  return { value: held.data, reused: true };
50
66
  const value = await step.compute();
51
- if ((step.store?.(value) ?? true) && session.unchanged(step.inputs))
67
+ if ((step.store?.(value) ?? true) && session.unchanged(step.inputs, step.captured))
52
68
  records.put({ path: step.path, step: step.step, hash: step.key, findings: step.compact(value), data: value });
53
69
  return { value, reused: false };
54
70
  }
package/cache/run.d.mts CHANGED
@@ -17,7 +17,7 @@ export interface AppRunInput {
17
17
  readonly presets?: Presets | undefined;
18
18
  readonly prettier?: Prettier | undefined;
19
19
  readonly edition?: string | undefined;
20
- /** Limits the formatter to these app-relative files (`app lint --changed`). */
20
+ /** Limits the formatter to these app-relative files. */
21
21
  readonly only?: readonly string[] | undefined;
22
22
  /** The ref the baseline database rules compare with, when the caller names one. */
23
23
  readonly base?: string | undefined;
package/cache/session.mjs CHANGED
@@ -1,4 +1,4 @@
1
- // session.mts - the disk-backed CheckSession of `starci app check` and `starci app lint`. It decides per family whether a run is full or diff
1
+ // session.mts - the disk-backed CheckSession of `starci app check`. It decides per family whether a run is full or diff
2
2
  // (the rules of contexts/project/check-cache.md "When a run is full"), serves the trusted records of a folder, takes the records a family
3
3
  // replaces and writes them once on flush: one JSON Lines file per area, byte-stable, only the files whose bytes changed. In CI it writes
4
4
  // nothing and compares the committed cache with what it inherited or computed instead.
@@ -127,7 +127,11 @@ export function createDiskSession(options) {
127
127
  replace(folder, records, replaceOptions) {
128
128
  replaced.set(folder, { records, depth: replaceOptions?.depth ?? 2 });
129
129
  },
130
- unchanged: (paths) => paths.every((file) => hashFile(path.join(root, file)) === index.hashOf(file)),
130
+ unchanged: (paths, captured) => paths.every((file) => {
131
+ const now = hashFile(path.join(root, file));
132
+ const taken = captured?.[file];
133
+ return taken === undefined ? now === index.hashOf(file) : (now ?? 'absent') === taken;
134
+ }),
131
135
  report: (report) => void reports.push(report),
132
136
  verdicts: () => verdictList,
133
137
  flush() {
@@ -3,6 +3,8 @@ export interface SideKey {
3
3
  readonly key: string;
4
4
  /** Every path the key covers: tracked paths of the side and the untracked program inputs. */
5
5
  readonly inputs: readonly string[];
6
+ /** The hash each input had when the key was computed (`absent` for a missing one): the baseline `session.unchanged` compares an edit-during-the-run against. */
7
+ readonly hashes: Readonly<Record<string, string>>;
6
8
  }
7
9
  /** The untracked files the side's TypeScript program includes (generated code), side-relative paths prefixed with `side/`. */
8
10
  export declare function untrackedProgramInputs(root: string, side: string, tracked: ReadonlySet<string>): string[];
@@ -43,5 +43,9 @@ export function sideKey(root, index, side) {
43
43
  const untracked = untrackedProgramInputs(root, side, new Set(index.paths));
44
44
  for (const file of untracked)
45
45
  parts[`untracked:${file}`] = hashFile(path.join(root, file)) ?? 'absent';
46
- return { key: combineHashes(parts), inputs: [...trackedPaths, ...untracked] };
46
+ const hashes = {};
47
+ for (const [name, hash] of Object.entries(parts))
48
+ if (name.startsWith('tracked:') || name.startsWith('untracked:'))
49
+ hashes[name.slice(name.indexOf(':') + 1)] = hash;
50
+ return { key: combineHashes(parts), inputs: [...trackedPaths, ...untracked], hashes };
47
51
  }
@@ -0,0 +1,34 @@
1
+ import type { RuleMemo } from '../runtime/scripts/hfs/rule-memo.mjs';
2
+ import type { RecordSet } from './records.mjs';
3
+ import type { CheckSession } from './types.mjs';
4
+ export declare const FILE_RULES_STEP = "file-rules";
5
+ export declare const RULE_STEP_PREFIX = "rule:";
6
+ export declare const UNIT_STEP_PREFIX = "unit:";
7
+ /** What the memo did, for the family's diagnosis line. */
8
+ export interface MemoCounts {
9
+ /** Files whose per-file rules were judged afresh / taken from a record. */
10
+ filesChecked: number;
11
+ filesReused: number;
12
+ /** Cross-file rules run afresh / taken from a record. */
13
+ keyedChecked: number;
14
+ keyedReused: number;
15
+ /** Per-file units of cross-file rules judged afresh / taken from a record. */
16
+ unitsChecked: number;
17
+ unitsReused: number;
18
+ }
19
+ export interface StructureMemo {
20
+ readonly memo: RuleMemo;
21
+ readonly counts: MemoCounts;
22
+ /** The app paths whose per-file records this run holds (the rest are dropped with their files). */
23
+ readonly files: Set<string>;
24
+ /** The app paths whose unit records this run holds, by unit step. */
25
+ readonly units: Map<string, Set<string>>;
26
+ }
27
+ /** The memo of one structure pass over the app at `root`. `reuse` is false when the family runs in full: the records are then written and never read. */
28
+ export declare function createStructureMemo(input: {
29
+ session: CheckSession;
30
+ records: RecordSet;
31
+ reuse: boolean;
32
+ root: string;
33
+ tracked: readonly string[];
34
+ }): StructureMemo;
@@ -0,0 +1,150 @@
1
+ // structure-memo.mts - the result cache of the content rules of the structure pass (`--gate hfs`, `--gate secrets`). The rules of the pass used to run over every tracked
2
+ // file on every run. Two kinds are now reused (contexts/project/check-cache.md "Which step reuses what"):
3
+ // - a per-file rule (`FileRule`: plaintext secrets, bypass markers, soft size): one record per file, `file-rules`, keyed on the file's content hash, the hashes of the
4
+ // other files the rule declares it reads (`deps`) and the names of the rules; it holds each rule's findings for the file. The cache folder is the only place a verdict is kept.
5
+ // - a cross-file rule (`memoKeyed`: the documentation freshness): one record per scope, `rule:<id>`, keyed on the exact set of files the rule read through `readText` /
6
+ // `readJson` (recorded when it ran, each with the hash it had then) and on the digest of the tracked path list, which every such rule derives its file sets from.
7
+ // Everything else the pass runs (the slots, the tree, the pins, the managed files, the rules that stat or walk the work tree, git state, untracked entries) stays always-run.
8
+ // A rule that must be judged on the work tree is never given to this memo; a finding that does not carry the path of the file it judges makes the whole batch uncacheable.
9
+ import path from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { loadTypescript } from '../runtime/scripts/lib/package-at.mjs';
12
+ import { recordReads } from '../runtime/scripts/hfs/rules/read.mjs';
13
+ import { inCacheDir } from './content-index.mjs';
14
+ import { combineHashes, hashFile, hashText } from './hash.mjs';
15
+ export const FILE_RULES_STEP = 'file-rules';
16
+ export const RULE_STEP_PREFIX = 'rule:';
17
+ export const UNIT_STEP_PREFIX = 'unit:';
18
+ const posix = (value) => value.split(path.sep).join('/');
19
+ const compact = (finding) => ({
20
+ path: finding.path ?? '.', line: finding.line ?? 0, column: finding.column ?? 0, rule: finding.code, message: String(finding.message ?? ''),
21
+ });
22
+ const plain = (value) => JSON.parse(JSON.stringify(value));
23
+ /** The memo of one structure pass over the app at `root`. `reuse` is false when the family runs in full: the records are then written and never read. */
24
+ export function createStructureMemo(input) {
25
+ const { session, records, reuse, root } = input;
26
+ const counts = { filesChecked: 0, filesReused: 0, keyedChecked: 0, keyedReused: 0, unitsChecked: 0, unitsReused: 0 };
27
+ const files = new Set();
28
+ const units = new Map();
29
+ // What this run already judged, so a rule asked twice about one file in one run (the schedule scan is) answers from the first judgement.
30
+ const judged = new Map();
31
+ const textHashes = new Map();
32
+ // The hash of an app path now: the index's for a tracked file (the snapshot of this run), the work tree's for anything else, `absent` for a missing one.
33
+ const hashNow = (appPath) => session.index.hashOf(appPath) ?? hashFile(path.join(root, appPath)) ?? 'absent';
34
+ // What every cross-file rule derives its file sets and its parsing from: the tracked path list, and the TypeScript the rules parse with (the lockfile is a fingerprint part, but an app
35
+ // with no install resolves the compiler from the harness instead).
36
+ let trackedDigest;
37
+ const typescriptVersion = () => loadTypescript(root, path.dirname(fileURLToPath(import.meta.url)))?.version ?? 'none';
38
+ const digestOfTracked = () => (trackedDigest ??= hashText(`${[...input.tracked].sort().join('\n')}\0typescript=${typescriptVersion()}`));
39
+ const prefixOf = (scopeRoot) => {
40
+ const relative = posix(path.relative(root, scopeRoot));
41
+ return relative === '' ? '' : `${relative}/`;
42
+ };
43
+ const filesStep = (scopeRoot, list, rules) => {
44
+ const prefix = prefixOf(scopeRoot);
45
+ const names = rules.map((rule) => rule.id).join('+');
46
+ // The cache folder is rewritten by every run and is outside every step's file set: its files are judged live and never keyed.
47
+ const live = list.filter((file) => inCacheDir(`${prefix}${file}`));
48
+ const liveOut = live.length === 0 ? [] : rules.map((rule) => rule.run([...live]));
49
+ const entries = list.filter((file) => !inCacheDir(`${prefix}${file}`)).map((file) => {
50
+ const deps = [...new Set(rules.flatMap((rule) => rule.deps?.(file) ?? []))].sort().map((dep) => `${prefix}${dep}`);
51
+ const parts = { rules: names, self: hashNow(`${prefix}${file}`) };
52
+ const captured = { [`${prefix}${file}`]: parts['self'] };
53
+ for (const dep of deps) {
54
+ parts[`dep:${dep}`] = hashNow(dep);
55
+ captured[dep] = parts[`dep:${dep}`];
56
+ }
57
+ return { file, app: `${prefix}${file}`, deps, captured, key: combineHashes(parts) };
58
+ });
59
+ const held = new Map();
60
+ for (const entry of entries) {
61
+ const record = reuse ? records.matching(entry.app, FILE_RULES_STEP, entry.key) : undefined;
62
+ if (record?.data !== undefined)
63
+ held.set(entry.file, record.data);
64
+ }
65
+ const missing = entries.filter((entry) => !held.has(entry.file));
66
+ counts.filesReused += entries.length - missing.length;
67
+ counts.filesChecked += missing.length;
68
+ const computed = new Map(missing.map((entry) => [entry.file, {}]));
69
+ if (missing.length > 0) {
70
+ const names = new Set(missing.map((entry) => entry.file));
71
+ for (const rule of rules) {
72
+ for (const finding of rule.run(missing.map((entry) => entry.file))) {
73
+ const own = finding.path !== undefined && names.has(finding.path) ? computed.get(finding.path) : undefined;
74
+ // A finding on a file the batch did not hold is a rule that is not per-file: nothing of it can be kept per file.
75
+ if (own === undefined)
76
+ return rules.flatMap((each) => each.run([...list]));
77
+ (own[rule.id] ??= []).push(finding);
78
+ }
79
+ }
80
+ for (const entry of missing) {
81
+ const result = computed.get(entry.file);
82
+ if (!session.unchanged(Object.keys(entry.captured), entry.captured))
83
+ continue;
84
+ records.put({ path: entry.app, step: FILE_RULES_STEP, hash: entry.key, findings: Object.values(result).flat().map(compact), data: plain(result) });
85
+ }
86
+ }
87
+ for (const entry of entries)
88
+ files.add(entry.app);
89
+ const out = [];
90
+ rules.forEach((rule, at) => {
91
+ for (const entry of entries)
92
+ out.push(...((held.get(entry.file) ?? computed.get(entry.file))?.[rule.id] ?? []));
93
+ out.push(...(liveOut[at] ?? []));
94
+ });
95
+ return out;
96
+ };
97
+ const keyedStep = (scopeRoot, id, run) => {
98
+ const at = prefixOf(scopeRoot).replace(/\/$/, '') || '.';
99
+ const step = `${RULE_STEP_PREFIX}${id}`;
100
+ const digest = digestOfTracked();
101
+ const record = reuse ? records.get(at, step) : undefined;
102
+ const data = record?.data;
103
+ if (data?.tracked === digest && data.reads !== undefined && data.findings !== undefined && Object.entries(data.reads).every(([file, hash]) => hashNow(file) === hash)) {
104
+ counts.keyedReused += 1;
105
+ return data.findings;
106
+ }
107
+ counts.keyedChecked += 1;
108
+ const reads = {};
109
+ const findings = recordReads((from, rel) => {
110
+ const app = posix(path.relative(root, path.join(from, rel)));
111
+ if (app.startsWith('..') || path.isAbsolute(app) || app in reads)
112
+ return;
113
+ reads[app] = hashNow(app);
114
+ }, run);
115
+ if (session.unchanged(Object.keys(reads), reads)) {
116
+ const stored = { path: at, step, hash: combineHashes({ tracked: digest, ...Object.fromEntries(Object.entries(reads).map(([file, hash]) => [`read:${file}`, hash])) }), findings: findings.map(compact), data: plain({ tracked: digest, reads, findings }) };
117
+ records.put(stored);
118
+ }
119
+ return findings;
120
+ };
121
+ const unitStep = (scopeRoot, id, file, extra, empty, compute) => {
122
+ const app = `${prefixOf(scopeRoot)}${file}`;
123
+ const step = `${UNIT_STEP_PREFIX}${id}`;
124
+ let extraHash = textHashes.get(extra);
125
+ if (extraHash === undefined)
126
+ textHashes.set(extra, (extraHash = hashText(extra)));
127
+ const self = hashNow(app);
128
+ const key = combineHashes({ unit: id, extra: extraHash, self, typescript: typescriptVersion() });
129
+ (units.get(step) ?? units.set(step, new Set()).get(step))?.add(app);
130
+ const memory = judged.get(JSON.stringify([step, app, key]));
131
+ if (memory !== undefined)
132
+ return memory;
133
+ const record = reuse ? records.matching(app, step, key) : undefined;
134
+ let value;
135
+ if (record !== undefined) {
136
+ counts.unitsReused += 1;
137
+ value = (record.data ?? empty);
138
+ }
139
+ else {
140
+ counts.unitsChecked += 1;
141
+ value = compute();
142
+ const empties = JSON.stringify(empty) === JSON.stringify(value);
143
+ if (session.unchanged([app], { [app]: self }))
144
+ records.put({ path: app, step, hash: key, findings: [], ...(empties ? {} : { data: plain(value) }) });
145
+ }
146
+ judged.set(JSON.stringify([step, app, key]), value);
147
+ return value;
148
+ };
149
+ return { memo: { files: filesStep, keyed: keyedStep, unit: unitStep }, counts, files, units };
150
+ }
package/cache/types.d.mts CHANGED
@@ -91,8 +91,12 @@ export interface CheckSession {
91
91
  replace(folder: string, records: readonly CacheRecord[], options?: {
92
92
  readonly depth?: number;
93
93
  }): void;
94
- /** True when none of `paths` changed in the work tree since the run began: a scoped step stores nothing keyed on a file edited during the run. */
95
- unchanged(paths: readonly string[]): boolean;
94
+ /**
95
+ * True when none of `paths` changed in the work tree since the run began: a scoped step stores nothing keyed on a file edited during the run.
96
+ * A path the content index does not hold (an untracked program input, generated code) is compared with `captured`, the hash the step's key took
97
+ * of it (`absent` for a missing file); without a captured hash it is compared with the index, so an untracked path is never unchanged.
98
+ */
99
+ unchanged(paths: readonly string[], captured?: Readonly<Record<string, string>>): boolean;
96
100
  report(report: FamilyReport): void;
97
101
  readonly progress: ProgressSink;
98
102
  flush(): FlushResult;
@@ -27,7 +27,7 @@ export async function runWiringFamily(session, { repoRoot, edition, files, run,
27
27
  };
28
28
  const step = session.progress.step('wiring', 'boot proof', Math.max(1, key.inputs.length));
29
29
  const { value, reused } = await scopedStep(session, records, {
30
- path: 'be', step: 'boot-proof', key: key.key, reuse: decision.mode === 'diff', inputs: key.inputs,
30
+ path: 'be', step: 'boot-proof', key: key.key, reuse: decision.mode === 'diff', inputs: key.inputs, captured: key.hashes,
31
31
  compute: async () => JSON.parse(JSON.stringify(await prove())),
32
32
  compact: (found) => found.findings.map(compact),
33
33
  store: (found) => !found.findings.some((finding) => finding.code === BOOT_PROOF_TIMEOUT),
@@ -0,0 +1,48 @@
1
+ /** The TypeScript compiler API of the app's own install (loadTypescript), typed by the version this package develops against. */
2
+ type Ts = typeof import('typescript');
3
+ /** One move of the report: a directive taken from `line` to the top of its module. */
4
+ export interface DirectiveFirstChange {
5
+ file: string;
6
+ line: number;
7
+ column: number;
8
+ code: string;
9
+ from: string;
10
+ to: string;
11
+ }
12
+ /** One case left for a person. */
13
+ export interface DirectiveFirstFinding {
14
+ file: string;
15
+ line: number;
16
+ column: number;
17
+ code: string;
18
+ reason: string;
19
+ }
20
+ /** The report of a migration (`starci app codemod directive-first`). */
21
+ export interface DirectiveFirstReport {
22
+ schema: string;
23
+ root: string;
24
+ write: boolean;
25
+ filesScanned: number;
26
+ filesChanged: string[];
27
+ changes: DirectiveFirstChange[];
28
+ findings: DirectiveFirstFinding[];
29
+ }
30
+ /** Move every `"use client"` / `"use server"` that is not in the prologue of the module to the top of it, as its first statement, with one blank line after. */
31
+ export declare function transformDirectiveFirst({ ts, text, file }: {
32
+ ts: Ts;
33
+ text: string;
34
+ file?: string;
35
+ }): {
36
+ text: string;
37
+ changes: DirectiveFirstChange[];
38
+ findings: DirectiveFirstFinding[];
39
+ };
40
+ /** Preview or apply the moves in one app (`be/`, `fe/`, or `src/` of a single-side app); a conflicting module is kept in the returned report. */
41
+ export declare function migrateDirectiveFirst({ root, write, ts }: {
42
+ root: string;
43
+ write?: boolean;
44
+ ts?: Ts | null;
45
+ }): DirectiveFirstReport;
46
+ /** Render the moves and what is left for a person as Markdown; JSON consumers read the same report object. */
47
+ export declare function directiveFirstMarkdown(report: DirectiveFirstReport): string;
48
+ export {};