@starci/hfs 5.0.7 → 5.0.8

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 (62) hide show
  1. package/CHANGELOG.md +11 -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 +143 -11
  7. package/cache/lint-family.d.mts +0 -2
  8. package/cache/lint-family.mjs +54 -33
  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/small-renames.mjs +22 -2
  20. package/lint/eslint-list.cjs +26 -0
  21. package/lint/run.d.mts +18 -58
  22. package/lint/run.mjs +54 -284
  23. package/package.json +1 -1
  24. package/release/lint-report.d.mts +1 -1
  25. package/release/lint-report.mjs +3 -3
  26. package/report/sonar.d.mts +1 -1
  27. package/report/sonar.mjs +3 -3
  28. package/report/stylelint-findings.d.mts +23 -0
  29. package/report/stylelint-findings.mjs +9 -0
  30. package/runtime/knowledge/hfs/canon-pins.yaml +7 -7
  31. package/runtime/knowledge/hfs/ci-standard.yaml +2 -2
  32. package/runtime/knowledge/hfs/rules.yaml +4 -4
  33. package/runtime/knowledge/hfs/slots.yaml +1 -1
  34. package/runtime/knowledge/sonar-gate.yaml +2 -2
  35. package/runtime/modules/kernel/failure-codes.yaml +4 -4
  36. package/runtime/scripts/api/node/run-node.d.mts +5 -0
  37. package/runtime/scripts/hfs/check.d.mts +16 -1
  38. package/runtime/scripts/hfs/check.mjs +18 -7
  39. package/runtime/scripts/hfs/gate-threshold.mjs +1 -1
  40. package/runtime/scripts/hfs/rule-memo.d.mts +30 -0
  41. package/runtime/scripts/hfs/rule-memo.mjs +24 -0
  42. package/runtime/scripts/hfs/rules/a15-findings.d.mts +6 -0
  43. package/runtime/scripts/hfs/rules/a15-findings.mjs +11 -2
  44. package/runtime/scripts/hfs/rules/ci-standard.mjs +2 -2
  45. package/runtime/scripts/hfs/rules/monorepo.d.mts +1 -3
  46. package/runtime/scripts/hfs/rules/monorepo.mjs +9 -11
  47. package/runtime/scripts/hfs/rules/pipeline.mjs +13 -9
  48. package/runtime/scripts/hfs/rules/read.d.mts +5 -0
  49. package/runtime/scripts/hfs/rules/read.mjs +13 -0
  50. package/runtime/scripts/hfs/rules/secrets.d.mts +2 -0
  51. package/runtime/scripts/hfs/rules/secrets.mjs +4 -1
  52. package/runtime/scripts/hfs/rules/stacks-release.mjs +35 -21
  53. package/runtime/scripts/lib/worktree-exclude.mjs +1 -1
  54. package/scaffold/app.mjs +3 -3
  55. package/src/check-cache-structure-support.d.mts +52 -0
  56. package/src/main.d.mts +3 -2
  57. package/src/main.mjs +123 -109
  58. package/sync/ci-commands.d.mts +2 -1
  59. package/sync/ci-commands.mjs +19 -20
  60. package/sync/index.mjs +2 -2
  61. package/templates/app/package-scripts/core.json +2 -2
  62. package/templates/app/task-graph/turbo.json +0 -3
@@ -1,7 +1,8 @@
1
- // pipeline.mjs - HFS_CI_MISSING_CANON (R13): CI runs the pinned `starci app lint`.
2
- // .github/workflows/ci.yml a `run:` step that runs the one lint entry: `starci app lint`,
3
- // `npx starci-harness[@<version>] app lint`, or `npm run <script>` whose command runs `starci app lint`
4
- // (`npm run lint`, arguments after `--` allowed); a version named in the step is the starci-harness pin,
1
+ // pipeline.mjs - HFS_CI_MISSING_CANON (R13): CI runs the pinned lint gate of `starci app check`.
2
+ // .github/workflows/ci.yml a `run:` step that runs the lint gate: `starci app check --gate lint`,
3
+ // `npx starci-harness[@<version>] app check --gate lint`, or `npm run <script>` whose command runs it
4
+ // (`npm run lint`, arguments after `--` allowed);
5
+ // a version named in the step is the starci-harness pin,
5
6
  // the version of the CLI that runs the check (canon-pins.mjs; an installed one is pinned by the pin check, R15)
6
7
  // (the push hook is no lint or typecheck step: it checks the release gate, scripts/hfs/runtime-rules/hook-shape.mjs keeps its shape)
7
8
  // A missing file is the slot manifest's finding (HFS_SLOT_REQUIRED_MISSING); a file that is present and lacks the step is this rule's.
@@ -13,7 +14,8 @@ const CI_FILE = '.github/workflows/ci.yml';
13
14
  const hfsPackage = 'starci-harness';
14
15
  const RUN_PREFIX = /^\s*(?:-\s+)?run:/;
15
16
  const NPX_PREFIX = /^npx\s+(?:(?:--no-install|-y)\s+)?/;
16
- const LINT_COMMAND = /^(?:starci-harness(?:@(\S+))?|starci)\s+app\s+lint/;
17
+ const LINT_COMMAND = /^(?:starci-harness(?:@(\S+))?|starci)\s+app\s+check/;
18
+ const GATE_ARGUMENT = /(?:^|\s)--gate(?:=|\s+)(\S+)/g;
17
19
  const NPM_RUN_PREFIX = 'npm run ';
18
20
  const EXAMPLE_RUN_PREFIX = 'node node_modules/@starci/hfs/runtime/scripts/hfs/example-secrets.mjs --run . -- ';
19
21
  const SCRIPT_NAME = /^[\w:.-]+/;
@@ -56,7 +58,9 @@ function parseNpmRun(command) {
56
58
  const args = separator ? tail.slice(separator[0].length) : null;
57
59
  return args !== null && !LINE_TERMINATOR.test(args) ? { script, args } : null;
58
60
  }
59
- /** { version } of a `starci app lint` command (`version` undefined when no starci-harness version is named), else null. */
61
+ /** Whether the arguments of an `app check` select the lint gate: `--gate lint` (or `--gate=lint`) among them. */
62
+ const runsLintGate = (tail) => [...tail.matchAll(GATE_ARGUMENT)].some((gate) => gate[1] === 'lint');
63
+ /** { version } of a `starci app check` command that runs the lint gate (`version` undefined when no starci-harness version is named), else null. */
60
64
  function lintCommandOf(command) {
61
65
  const rest = command.replace(NPX_PREFIX, '');
62
66
  const match = LINT_COMMAND.exec(rest);
@@ -65,7 +69,7 @@ function lintCommandOf(command) {
65
69
  const tail = rest.slice(match[0].length);
66
70
  if (tail && !(WHITESPACE.test(tail[0]) && !LINE_TERMINATOR.test(tail.slice(1))))
67
71
  return null;
68
- return { version: match[1] };
72
+ return runsLintGate(tail) ? { version: match[1] } : null;
69
73
  }
70
74
  /** `npm run <script> -- <args>` spelled out as the root package.json script it runs; any other command is itself. */
71
75
  function expanded(command, scripts) {
@@ -87,9 +91,9 @@ export function pipelineFindings({ repoRoot, files, pins }) {
87
91
  const selected = isRuntimeExampleApp(repoRoot) ? commands.map(exampleCommandOf) : commands;
88
92
  const lints = selected.flatMap((command) => chainedCommands(expanded(command, scripts))).map(lintCommandOf).filter(Boolean);
89
93
  if (!lints.length)
90
- findings.push(found(CI_MISSING_CANON, CI_FILE, `${CI_FILE} has no step that runs \`starci app lint\` (directly or through \`npm run lint\`); CI runs the one lint entry, which includes the repository check`, { step: 'starci app lint' }));
94
+ findings.push(found(CI_MISSING_CANON, CI_FILE, `${CI_FILE} has no step that runs \`starci app check --gate lint\` (directly or through \`npm run lint\`); CI runs the lint gate of the one app check`, { step: 'starci app check --gate lint' }));
91
95
  else if (pinned && !lints.some((match) => match.version === undefined || match.version === pinned)) {
92
- findings.push(found(CI_MISSING_CANON, CI_FILE, `${CI_FILE} runs starci app lint at ${lints[0].version}, but ${hfsPackage} is pinned at ${pinned}; run the pinned version`, { step: 'starci app lint', pinned }));
96
+ findings.push(found(CI_MISSING_CANON, CI_FILE, `${CI_FILE} runs starci app check at ${lints[0].version}, but ${hfsPackage} is pinned at ${pinned}; run the pinned version`, { step: 'starci app check --gate lint', pinned }));
93
97
  }
94
98
  }
95
99
  return findings;
@@ -1,7 +1,12 @@
1
1
  import type { Finding, Parsed } from './rule-types.mjs';
2
+ /** Who is told which file a rule reads: the result cache records the read set of a cross-file rule through it. */
3
+ type ReadSink = (repoRoot: string, rel: string) => void;
4
+ /** Runs `run` with every `readText` / `readJson` it makes reported to `onRead` (before the read), and returns its value. */
5
+ export declare function recordReads<T>(onRead: ReadSink, run: () => T): T;
2
6
  /** The text of `rel` under `repoRoot`, or null when it is absent, unreadable, over MAX_READ_BYTES or binary. */
3
7
  export declare function readText(repoRoot: string, rel: string): string | null;
4
8
  /** The JSON of `rel`, or null when it is absent or not JSON. */
5
9
  export declare function readJson(repoRoot: string, rel: string): Parsed | null;
6
10
  /** One error finding of `code` on `file`; `extra` carries the finding's own fields. */
7
11
  export declare const found: (code: string, file: string, message: string, extra?: Record<string, unknown>) => Finding;
12
+ export {};
@@ -3,8 +3,21 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  /** Files above this size are not read by a tree check (a tracked binary or a generated dump is no source of these laws). */
5
5
  const MAX_READ_BYTES = 1024 * 1024;
6
+ let sink = null;
7
+ /** Runs `run` with every `readText` / `readJson` it makes reported to `onRead` (before the read), and returns its value. */
8
+ export function recordReads(onRead, run) {
9
+ const before = sink;
10
+ sink = onRead;
11
+ try {
12
+ return run();
13
+ }
14
+ finally {
15
+ sink = before;
16
+ }
17
+ }
6
18
  /** The text of `rel` under `repoRoot`, or null when it is absent, unreadable, over MAX_READ_BYTES or binary. */
7
19
  export function readText(repoRoot, rel) {
20
+ sink?.(repoRoot, rel);
8
21
  try {
9
22
  const target = path.join(repoRoot, rel);
10
23
  if (fs.statSync(target).size > MAX_READ_BYTES)
@@ -17,6 +17,8 @@ export declare function secretFileFindings({ file, text, manifestText }: {
17
17
  text: string | null;
18
18
  manifestText?: string | null | undefined;
19
19
  }): import("./rule-types.mjs").Finding[];
20
+ /** The application-stacks manifest beside a README (the one other file a README's secret findings read), in the README's own scope; null for any other file. */
21
+ export declare const readmeManifestOf: (file: string) => string[] | null;
20
22
  /** The findings of R06 over `files` (tracked paths) of `repoRoot`; `resolver` names the slots. */
21
23
  export declare function secretFindings({ repoRoot, files, resolver }: {
22
24
  repoRoot: string;
@@ -51,6 +51,8 @@ export function secretFileFindings({ file, text, manifestText }) {
51
51
  }
52
52
  return findings;
53
53
  }
54
+ /** The application-stacks manifest beside a README (the one other file a README's secret findings read), in the README's own scope; null for any other file. */
55
+ export const readmeManifestOf = (file) => (/(?:^|\/)README\.md$/.test(file) ? [path.posix.join(path.posix.dirname(file), '.starcistacks/application-stacks.yaml')] : null);
54
56
  /** The findings of R06 over `files` (tracked paths) of `repoRoot`; `resolver` names the slots. */
55
57
  export function secretFindings({ repoRoot, files, resolver }) {
56
58
  const findings = [];
@@ -61,7 +63,8 @@ export function secretFindings({ repoRoot, files, resolver }) {
61
63
  findings.push(found(PLAINTEXT_SECRET, file, `${file} is a plaintext secret file tracked in ${slot.slot}${destination}`, { slot: slot.slot, goesTo: slot.goesTo }));
62
64
  continue;
63
65
  }
64
- const manifestText = /(?:^|\/)README\.md$/.test(file) ? readText(repoRoot, path.posix.join(path.posix.dirname(file), '.starcistacks/application-stacks.yaml')) : undefined;
66
+ const manifestPath = readmeManifestOf(file)?.[0];
67
+ const manifestText = manifestPath === undefined ? undefined : readText(repoRoot, manifestPath);
65
68
  findings.push(...secretFileFindings({ file, text: readText(repoRoot, file), manifestText }));
66
69
  }
67
70
  return findings;
@@ -6,6 +6,7 @@ import { loadTypescript } from '../../lib/package-at.mjs';
6
6
  import { walkSqlAst, sqlParser, sqlCallArgument } from '../../lib/sql-ast.mjs';
7
7
  import { isPlainObject as plain } from '../../../engine/plain-object.mjs';
8
8
  import { found, readText } from './read.mjs';
9
+ import { memoUnit } from '../rule-memo.mjs';
9
10
  import { stacksDeploymentContexts } from './stacks-runtime.mjs';
10
11
  // PostgreSQL initializes asynchronously, while HFS finders must stay synchronous.
11
12
  // Optional loading also lets dependency-free relocated development gates import this rule.
@@ -39,20 +40,26 @@ function safeRelative(relative) {
39
40
  return false;
40
41
  return relative.split('/').every((part) => part && part !== '.' && part !== '..');
41
42
  }
42
- function publicReference(repoRoot, files, relative) {
43
+ /** `links` remembers which directories are symbolic links for the duration of one scan, so a directory shared by many files is asked once. */
44
+ function publicReference(repoRoot, files, relative, links = new Map()) {
43
45
  if (!repoRoot || !safeRelative(relative))
44
46
  return false;
45
- if (!files.includes(relative))
47
+ if (!(files instanceof Set ? files.has(relative) : files.includes(relative)))
46
48
  return false;
47
49
  const root = path.resolve(repoRoot), target = path.resolve(root, relative);
48
50
  try {
49
- let cursor = target;
50
- while (cursor !== root) {
51
- if (fs.lstatSync(cursor).isSymbolicLink())
51
+ const stat = fs.lstatSync(target);
52
+ if (stat.isSymbolicLink())
53
+ return false;
54
+ for (let cursor = path.dirname(target); cursor !== root; cursor = path.dirname(cursor)) {
55
+ let link = links.get(cursor);
56
+ if (link === undefined) {
57
+ link = fs.lstatSync(cursor).isSymbolicLink();
58
+ links.set(cursor, link);
59
+ }
60
+ if (link)
52
61
  return false;
53
- cursor = path.dirname(cursor);
54
62
  }
55
- const stat = fs.lstatSync(target);
56
63
  return stat.isFile() && stat.size > 0 && stat.size <= MAX_BYTES;
57
64
  }
58
65
  catch {
@@ -356,30 +363,37 @@ function templateObservations(node, file) {
356
363
  return [{ id: null, cadence: null, at: file }];
357
364
  return sqlObservations(named, file);
358
365
  }
359
- function recurringObservations({ doc, repoRoot, files, file, issues }) {
360
- const sql = file.startsWith('db/migrations/') && file.endsWith('.sql') && !file.startsWith('db/migrations/archive/');
361
- const sourceFile = SOURCE.test(file) && !TEST.test(file);
362
- if ((!sql && !sourceFile) || !publicReference(repoRoot, files, file))
363
- return [];
366
+ function observationsOf({ doc, repoRoot, file, sql }) {
364
367
  const text = readText(repoRoot, file);
365
368
  if (text === null)
366
- return [];
369
+ return { observations: [], issues: [] };
367
370
  if (sql)
368
- return sqlObservations(text, file);
371
+ return { observations: sqlObservations(text, file), issues: [] };
369
372
  ts ??= loadTypescript(repoRoot, HERE);
370
- if (!ts) {
371
- issues.push(scheduleIssue(file, 'TypeScript is required to inspect recurring application work'));
372
- return [];
373
- }
373
+ if (!ts)
374
+ return { observations: [], issues: [scheduleIssue(file, 'TypeScript is required to inspect recurring application work')] };
374
375
  const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
375
- issues.push(...sourceObservationIssues(doc, file, source, sourceBindings(source)));
376
- return sourceSqlObservations(source, file);
376
+ const issues = sourceObservationIssues(doc, file, source, sourceBindings(source));
377
+ return { observations: sourceSqlObservations(source, file), issues };
378
+ }
379
+ function recurringObservations({ doc, repoRoot, files, file, issues, links, tools }) {
380
+ const sql = file.startsWith('db/migrations/') && file.endsWith('.sql') && !file.startsWith('db/migrations/archive/');
381
+ const sourceFile = SOURCE.test(file) && !TEST.test(file);
382
+ if ((!sql && !sourceFile) || !publicReference(repoRoot, files, file, links))
383
+ return [];
384
+ // The result cache keeps this per file: the bytes of the file, the manifest and the parsers decide it (contexts/project/check-cache.md).
385
+ const one = memoUnit(repoRoot, 'recurring-observations', file, `${tools}
386
+ ${JSON.stringify(doc)}`, { observations: [], issues: [] }, () => observationsOf({ doc, repoRoot, file, sql }));
387
+ issues.push(...one.issues);
388
+ return one.observations;
377
389
  }
378
390
  function recurringSourceIssues({ doc, repoRoot, files }) {
379
391
  const issues = [], cron = new Map();
380
392
  const paths = [...new Set(files)].sort((left, right) => left.localeCompare(right));
393
+ const tracked = new Set(paths), links = new Map();
394
+ const tools = `typescript=${loadTypescript(repoRoot, HERE) !== null} sql=${pg !== null}`;
381
395
  for (const file of paths) {
382
- const observations = recurringObservations({ doc, repoRoot, files, file, issues });
396
+ const observations = recurringObservations({ doc, repoRoot, files: tracked, file, issues, links, tools });
383
397
  for (const observation of observations) {
384
398
  if ('remove' in observation) {
385
399
  if (observation.remove)
@@ -5,7 +5,7 @@
5
5
  // <repo>/.starciwork/worktrees/<name> one runtime scratch tree
6
6
  // The directory is git-excluded (.git/info/exclude), so `git status`,
7
7
  // `git ls-files --others --exclude-standard` and sonar never see it; every scanner that walks the filesystem
8
- // (canon-scan's eslint, starci app lint, the architecture walkers, input digests, proof-integrity, Work readers)
8
+ // (canon-scan's eslint, starci app check, the architecture walkers, input digests, proof-integrity, Work readers)
9
9
  // skips it with isWorktreesPath / WORKTREES_IGNORE_GLOBS, or it would lint and hash every sibling worktree.
10
10
  import path from 'node:path';
11
11
  import { posixPath } from './path-key.mjs';
package/scaffold/app.mjs CHANGED
@@ -21,7 +21,7 @@ import { HARNESS_PACKAGE, canonPins } from '../runtime/scripts/hfs/canon-pins.mj
21
21
  import { TEMPLATES_DIR, appSource, imageFiles, render, renderTargets, writeTargets } from '../sync/index.mjs';
22
22
  import { ScaffoldError } from './service.mjs';
23
23
  import { testWorldFiles } from './test-world.mjs';
24
- import { FE_APP_SCRIPTS, PACKAGE_MANAGER, WORKSPACES, WORKSPACE_LINT, feAppPackageName } from '../runtime/scripts/hfs/rules/monorepo.mjs';
24
+ import { FE_APP_SCRIPTS, PACKAGE_MANAGER, WORKSPACES, feAppPackageName } from '../runtime/scripts/hfs/rules/monorepo.mjs';
25
25
  import { isLinkLike } from '../runtime/scripts/api/fs/is-link-like.mjs';
26
26
  import { unlinkOnly } from '../runtime/scripts/api/fs/lib.mjs';
27
27
  import { initializeAppIdentity } from '../runtime/scripts/hfs/secret-init.mjs';
@@ -200,8 +200,8 @@ const FE_PACKAGES = Object.freeze({
200
200
  ui: { exports: ['.'], dependencies: { '@heroui/react': null, '@starci/grammar': null, '@starci/heroicons': null, '@xyflow/react': null, 'react-aria-components': null, 'next-intl': null, react: null, 'react-dom': null } },
201
201
  i18n: { exports: ['.', './proxy', './routing'], dependencies: { next: null, 'next-intl': null, 'server-only': null } },
202
202
  });
203
- /** The scripts of every fe package workspace: tsc builds it to dist and type-checks it; it lints with the workspace lint. */
204
- const PACKAGE_WORKSPACE_SCRIPTS = Object.freeze({ build: 'tsc -p tsconfig.build.json', lint: WORKSPACE_LINT, typecheck: 'tsc --noEmit -p tsconfig.json' });
203
+ /** The scripts of every fe package workspace: tsc builds it to dist and type-checks it. */
204
+ const PACKAGE_WORKSPACE_SCRIPTS = Object.freeze({ build: 'tsc -p tsconfig.build.json', typecheck: 'tsc --noEmit -p tsconfig.json' });
205
205
  /** The app hfs.json of a new app called `name`; full remains the byte-for-byte default declaration. */
206
206
  export const starterDeclaration = (name, manifest = loadSlotManifest(), edition = 'full') => ({
207
207
  hfs: manifest.major,
@@ -0,0 +1,52 @@
1
+ import type { TestContext } from 'node:test';
2
+ import { type PassResult } from '../cache/hfs-family.mjs';
3
+ import type { CacheRecord } from '../cache/types.mjs';
4
+ import type { MachineFn } from '../runtime/scripts/hfs/check.mjs';
5
+ export declare const PORTABLE: {
6
+ parts: {
7
+ name: string;
8
+ sha256: string;
9
+ }[];
10
+ portable: boolean;
11
+ localReasons: never[];
12
+ };
13
+ /** An AWS access key shape the secret scan refuses (built from parts: the fixture is data, not a credential). */
14
+ export declare const FAKE_KEY: string;
15
+ /** The files of the fixture app; the untracked ones (generated code) are written after the commit. */
16
+ export declare const FILES: Readonly<Record<string, string>>;
17
+ /** The untracked, git-ignored generated code the fe program includes. */
18
+ export declare const GENERATED = "fe/src/__generated__/graphql.ts";
19
+ export declare function write(root: string, file: string, text: string): void;
20
+ /** A temp git repository holding `files` (committed) and the generated input (not tracked). */
21
+ export declare function appRepo(t: TestContext, files?: Readonly<Record<string, string>>): string;
22
+ /** The architecture machine as a double that counts its runs per side (`calls`) and can act while it runs (`during`). */
23
+ export declare function machineDouble(during?: (side: string) => void): {
24
+ machine: MachineFn;
25
+ calls: string[];
26
+ };
27
+ export interface RunOptions {
28
+ readonly hfs?: boolean;
29
+ readonly secrets?: boolean;
30
+ readonly during?: (side: string) => void;
31
+ readonly fingerprint?: typeof PORTABLE;
32
+ readonly tracked?: readonly string[];
33
+ }
34
+ /** One structure pass over `root` against the committed cache of the work tree; the session is flushed. */
35
+ export declare function runPass(root: string, options?: RunOptions): Promise<{
36
+ pass: PassResult;
37
+ calls: string[];
38
+ hfs: readonly CacheRecord[];
39
+ secrets: readonly CacheRecord[];
40
+ }>;
41
+ /** `path\0step` to hash, for comparing which records a run rewrote. */
42
+ export declare const hashes: (records: readonly CacheRecord[]) => Map<string, string>;
43
+ /** The record keys whose hash differs between two runs, added or removed ones included. */
44
+ export declare function changed(before: readonly CacheRecord[], after: readonly CacheRecord[]): string[];
45
+ /** Every file-system access `run` makes under `root` (outside node_modules, which the lockfile in the fingerprint stands for), as `method path`. */
46
+ export declare function recordFs<T>(root: string, run: () => T): {
47
+ value: T;
48
+ accessed: Set<string>;
49
+ };
50
+ export declare const filesOf: (accessed: Set<string>) => Set<string>;
51
+ /** The fixture of the per-file rules: a secret, a bypass marker, a long source, a README beside a stack manifest and files that are clean. */
52
+ export declare const RULE_FILES: Readonly<Record<string, string>>;
package/src/main.d.mts CHANGED
@@ -49,10 +49,11 @@ interface AppIo {
49
49
  type MainOptions = {
50
50
  [K in keyof AppIo]?: AppIo[K] | undefined;
51
51
  };
52
+ /** An argument the parser refuses (an unknown flag, a missing value, a form a verb does not take): the one error `main` follows with the usage. */
53
+ export declare class UsageError extends Error {
54
+ }
52
55
  export declare const VERBS: readonly string[];
53
56
  export declare const USAGE: string;
54
- /** The nearest folder at or above `dir` that holds hfs.json (the app root of a workspace), or `dir` itself when none does (the lint then refuses it). */
55
- export declare function appRootAbove(dir: string): string;
56
57
  /**
57
58
  * `harness` ({ version }) is the CLI that runs this package in-process: its version is the starci-harness an app is judged and
58
59
  * scaffolded at (runtime/scripts/hfs/canon-pins.mjs). `run` injects the database runner, `runLinter` the linter transport, `checkApp`