Skip to content

Commit ebb66aa

Browse files
fix(spec): registration remedies name the entry file and gen:migration-registry, never the generated registry.ts (#21021)
Fixes #20809 Clause-②: no ## What this changes `RETIRED_KEYS_BY_MAJOR`, `RETIRED_DEFS_BY_MAJOR` and every step's `semantic` list are generated into `packages/spec/src/migrations/registry.ts` from one file per entry under `packages/spec/src/migrations/entries/`. Gate (b) of `packages/spec/scripts/build-schemas.ts`, the def-removal gate and four sibling remedies still told an author to paste lines into, or delete lines from, that generated file. An author who followed them typed between generated markers, which the next `gen:migration-registry` run reverts and `check:migration-registry` fails first. Every such remedy now names the entry file and the generator command. Where the gate knows the id and the major, it prints the exact file and the one declaration it carries. Where the major is the author's to establish (check (c) proof 1), it names the directory and `entries/README.md`. This is the route the retirement kit's checklist landed first (PR #20828, merged as `745ac30a`), quoted from `.claude/skills/spec-property-retirement/SKILL.md` on `main`: > 在 `packages/spec/src/migrations/entries/retired-keys/` 加一个文件(命名见 `entries/README.md`),再跑 `pnpm --filter @objectstack/spec gen:migration-registry` 把它生成进 `registry.ts`,⛔ 永不在那里手打。 Gate (b) before, as printed at `05be35259`: ```text 1. Declare each retirement by its EXACT key in RETIRED_KEYS_BY_MAJOR (packages/spec/src/migrations/registry.ts) — copy these lines in: 'api/HttpFindQueryParams:distinct', under `17: [ … ]` (create the major's array if it is the first). ``` After (the template rendered for the test's fixture key; the pins below assert it from a real run): ```text 1. Register each retirement by its EXACT key in RETIRED_KEYS_BY_MAJOR. That table is generated: add one entry file per key (named per packages/spec/src/migrations/entries/README.md; these names are the generator's own) — packages/spec/src/migrations/entries/retired-keys/17.api__HttpFindQueryParams__distinct.ts export const entry = 'api/HttpFindQueryParams:distinct'; — with a `//` comment above the `export` saying why the key was retired (it lands above the entry in the table), then run `pnpm --filter @objectstack/spec gen:migration-registry`. ⛔ Never type an entry into the generated table by hand: the next run reverts it, and check:migration-registry fails it first. ... ``` The two docblocks the card names now say what the `guidance` slot is for: prescriptions for keys the shape does not declare (wrong-layer pointers, and the upgrade for a spelling removed from the shape). A key retired in place is a `retiredKey()` tombstone in the shape, never a guidance row, because `shared/alias-integrity.test.ts` ("no guidance key is itself a declared key") makes a guidance row for a declared key dead. No guidance table entry changes. ## The dispatch's hypotheses, measured **H1 — every printed sentence that routes an author into `migrations/registry.ts`.** `git grep -n -E 'migrations/registry\.ts|copy these lines|step in src/migrations' -- packages/spec/scripts/build-schemas.ts` returns 10 lines at `05be35259` and 2 at this head (both code comments). Positive control: the PM's four named sites (`:789`, `:1128`, `:2637`, `:2836`) are among the 10. I also read the lines that name a table without a file. Verdicts (line numbers at `05be35259`): | Site | Printed | Verdict | |---|---|---| | `:789` manifest disappearance | "declare each one in RETIRED_DEFS_BY_MAJOR (src/migrations/registry.ts)" | Same route. It now prints one `entries/retired-defs/` file per def, plus the generator. | | `:1089` check (a), aged-out route | "requires the exact key be declared in RETIRED_KEYS_BY_MAJOR" | Correct as it stands. It states a precondition, names no file and asks for no edit. | | `:1100` | code comment, where the gate READS the table | Correct. Not printed. | | `:1127-1130` gate (b) | "copy these lines in" | Fixed (this card). | | `:1183-1185` (b2) | "delete the entry from packages/spec/src/migrations/registry.ts" | Same route. Each row now names its entry file (from the entry's own id and major, so a key carried through a declared def rename still names the file on disk), plus the generator. | | `:1222-1224` (b3) | same | Same route, same fix. It also says that a respelled id renames the file. | | `:2578-2579` def (b2) mirror | same | Same route, same fix. | | `:2619` | info line "RETIRED_DEFS_BY_MAJOR, major N" | Correct. A report, not a remedy. | | `:2636-2639` def-removal gate | "copy these lines in" | Same route (pointer `5908348030`). Fixed like gate (b). | | `:2781` check (c) violation row | "no entry in RETIRED_KEYS_BY_MAJOR names this EXACT key" | Correct. A diagnosis; the route is in the footer's proof 1. | | `:2824-2825` check (c) proof 1 | "(src/migrations/registry.ts) … writes it down" | Same route. It now names the entry directory, `entries/README.md` and the generator. It names the README rather than a file because the true major is the author's to establish. | | `:2835-2836` check (c) proof 3 | "(src/migrations/registry.ts)" | Same route. It now names the entry directory; the manifest gate it defers to prints the file. | | `:3261-3262` default-change remedy | "add a `semantic` entry to this major's step in src/migrations/registry.ts" | Same defect on the third generated table. Fixed in place under the bounded rule: same defect class, same file, mechanical, same gate family. | **H2 — the naming source.** `entries/README.md` defines a file as `MAJOR.ID.ts` with `/` and `:` replaced by `__`. The generator's `shardNameFor` is, in its own words, "the whole naming rule, in one place", and `loadEntries` refuses a file whose name does not match its id. Every remedy prints its file name through `shardNameFor` (with `KINDS` / `ENTRIES_DIR` for the directory), imported from the generator. No second copy of the convention exists, and the README stays the one human description. **H3 — whether the docblock text ships.** Measured after `pnpm --filter @objectstack/spec build`, with `npm pack --dry-run`: - `src/ai/tool.zod.ts` ships verbatim through `files[]` (`src/**/*.zod.ts`). - `src/shared/strict-object.ts` does not ship as source, but its edited JSDoc is in `dist/shared/index.d.ts` and `dist/shared/index.d.mts`. Positive control: the untouched `aliases` docblock is in the same two files. - Neither script ships. So this PR takes a `patch` changeset, `.changeset/20809-guidance-slot-docblocks.md`, and no `skip-changeset`. ## Landing site beyond the claimed surface: the generator `build-migration-registry.ts` ran its CLI at module top level, and in write mode it rewrites `registry.ts`, so nothing could import `shardNameFor` from it. The fix is at the producer: the CLI body moves into `main()`, behind an entry guard spelled the way `check-generated.ts` (same directory) spells it, with realpath on both sides. `shardNameFor`'s docblock now names its second reader. Why not move the rule into a new `scripts/lib/` module, or import `scripts/invoked-as.mjs`: CI runs `check:migration-registry` only when a changed path is in its selector arm (`scripts/ci/select-gate-families.sh`, `migration_registry`). That arm lists the generator's read set path by path. A new import would add an input outside the arm, and a later edit to that input alone would skip the gate. The guard adds only `node:fs`'s `realpathSync`, so the generator's read set is unchanged. New test `packages/spec/scripts/build-migration-registry-entry.test.ts` pins both directions of the guard, each as a real `tsx` process: a direct run reaches the generator, a run through a symlink also reaches it, and an import prints nothing beyond what the importer asked for. ## Tests (at `c2e5b3b81`) - `pnpm --filter @objectstack/spec exec vitest run --project repo scripts/build-schemas-check-mode.test.ts`, the whole file: **88 passed (88)**, Duration 995 s, lock verdict `command-exit 0`. - Gate (b): the remedy names `entries/retired-keys/` plus `shardNameFor(major, key)`, the declaration and the generator command. It contains no paste line, no `copy these lines` and no `migrations/registry.ts`. - New control: every file and declaration pair is parsed off the red run's own output and written into the sandbox. The sandbox's own generator (which refuses a misnamed file) then runs, and gate (b) is silent on the re-run. The existing "registered by exact name ⇒ (b) silent" case still passes. - The def-removal gate, the manifest disappearance check, (b2), def (b2), (b3) and the end-to-end unpublish repro each pin their entry file and the generator. - `pnpm --filter @objectstack/spec test` (the `local` project, which includes the new guard test): **585 files, 17208 passed | 1 todo**, `command-exit 0`. - `pnpm --filter @objectstack/spec typecheck` (`tsc` on src, `tsconfig.scripts.json` covering both changed scripts and the new test, and the test-typecheck ledger): `command-exit 0`. - `pnpm --filter @objectstack/spec check:migration-registry`: `--self-test: ok`, `src/migrations/registry.ts is current (330 semantic, 242 retired-key, 211 retired-def)`. This is byte-identical to the reading taken before any edit. ## Ablations (one-shot; each leg went through `scripts/ablation-replace.mjs`, which proves the mutation landed and the restore matched HEAD) - **A1 — guard deleted** (`main()` called unconditionally). The import probe goes red: the importer printed `✓ wrote src/migrations/registry.ts (330 semantic, 242 retired-key, 211 retired-def)`. Both run probes stay green. The first A1 attempt was refused by the tool before any run, because the replacement was a substring of the anchor and its count could not rise. It is recorded here as a no-op, and the re-run used a distinct marker. - **A3 — raw `argv[1]` comparison** (no realpath). The symlink run probe goes red: exit 0 with empty output, the silent no-op the guard spelling exists to prevent. The other two stay green. - **B — gate (b) reverted to paste lines.** Of the six cases in gate (b)'s describe block, two go red: the remedy case (no entry-file path) and the new follow-the-printed-route control (`[]` pairs parsed, against the 2 expected). The other 4 stay green. - Every restore was proven: blob equal to HEAD and `git diff HEAD` empty. ## Gates Derived from this diff with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: the change set is 7 paths against the merge base `05be35259`, giving 88 commands. All 88 were run at `c2e5b3b81`. The `--ran` reconciliation reads **88 derived — 86 run, 2 NOT-MEASURED, 0 UNRUN**. - **86 exit 0.** These include `check:migration-registry`, `check:authorable-surface`, `check:api-surface`, `check:docs`, `check:doc-authoring`, `check:nul-bytes`, `check:empty-changeset`, `check:changeset-no-major`, `check:cross-package-test-inputs`, `check:test-source-alias`, `check:type-check-coverage` and `check:issue-citations`. Three of them needed a second run: - `check:doc-formula-expressions` and `check:lean-entry-closure` first refused with exit 3 (prerequisite not met). Both passed after a `turbo run build` of their 14-package closure. - `check:pm-dispatch-gates` was first cut by this run's own 900 s per-command cap, which is not counted. The uncapped re-run printed `dispatch-gates self-test: 1976 cases pass` in 1259 s. Its exit 0 is read from the absence of pnpm's failure marker, not from a captured status. - **NOT MEASURED:** `check:dual-build-cjs-loads` and `check:type-check-debt`. Both exit 3 (prerequisite not met): they read every workspace package's `dist/`, which `lint.yml` builds first, and that whole-tree build was not run in this worktree on a shared 4-core box. CI runs them. - `pnpm --filter @objectstack/spec check:generated` (named by the dispatch): "All 15 generated artifacts are up to date", exit 0. - `origin/main` advanced 11 commits past the base during these runs. None of them touches these 7 paths or `src/migrations/`, so they are not merged here; the PR's CI runs on the merge ref. ## Acceptance notes - `scripts/lib/sharded-artifacts.ts` (about `:353`) writes the manifest shard `description` into every `json-schema.manifest/` shard, and that text still says "must be declared in RETIRED_DEFS_BY_MAJOR (src/migrations/registry.ts)". It is the same family as the H1 rows. It is not changed here, because changing it regenerates every manifest shard, the hottest generated artifact. Carrier: none. - `src/data/object.zod.ts`'s `UNKNOWN_KEY_GUIDANCE` has a section comment, "Tombstones for RETIRED keys", that uses the same loose term the two docblocks used. The card scoped this to two docblocks with no table edits. Carrier: none. - `scripts/lib/default-changes.ts` (about `:52`) has a code comment that still says "semantic entry to the major's step in src/migrations/registry.ts". The printed remedy it describes is fixed here. Carrier: none. - The changeset follows the dispatch's H3 rule (shipped text ⇒ `patch`) and AGENTS.md's `skip-changeset` criterion ("a diff that publishes nothing from any released package"). os-dev's fast path lists comments as non-publishing; this PR measured the shipped bytes instead. ## Review A docblock edit in `packages/spec/src` owes an at-tier contract review at landing (run by the seat). No governed surface is touched. --- _Generated by [Claude Code](https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8f78495 commit ebb66aa

7 files changed

Lines changed: 417 additions & 98 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
docs(spec): the `strictObject` `guidance` option and the `ToolSchema` guidance table no longer describe `guidance` rows as tombstones for retired keys
6+
7+
Clause-②: no
8+
9+
`StrictObjectOptions.guidance` (in the published declarations) and the docblock above
10+
`TOOL_RETIRED_KEY_GUIDANCE` in `src/ai/tool.zod.ts` (shipped as source) called the slot a
11+
place for "tombstones for retired keys". A tombstone is `retiredKey()` in the shape, which
12+
keeps the key declared, and a `guidance` row for a declared key never fires. The docblocks
13+
now say what the slot is for: prescriptions for keys the shape does not declare —
14+
wrong-layer pointers, and the upgrade for a spelling removed from the shape.
15+
16+
Text only: no schema, no guidance entry, no prescription and no parse behaviour changes.
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
//
3+
// `build-migration-registry.ts` has TWO kinds of caller now, and its entry guard
4+
// is the only thing telling them apart:
5+
//
6+
// - pnpm RUNS it (`gen:migration-registry`, `check:migration-registry`), and
7+
// write mode rewrites `src/migrations/registry.ts`;
8+
// - `build-schemas.ts` IMPORTS it, for `shardNameFor` — so the entry file each
9+
// registration remedy names is the one name the generator accepts, read from
10+
// the rule `entries/README.md` documents instead of restated beside it.
11+
//
12+
// Both directions are pinned, because each one fails silently on its own. A
13+
// guard that answered "imported" for a real run turns the CI gate into exit 0
14+
// with no output; one that answered "run" for an import executes the generator
15+
// inside every build of the schemas. Each probe is a real `tsx` process, the
16+
// runner both callers use, so neither half is a model of the other.
17+
18+
import { describe, it, expect, afterAll } from 'vitest';
19+
import { spawnSync } from 'node:child_process';
20+
import fs from 'node:fs';
21+
import os from 'node:os';
22+
import path from 'node:path';
23+
import { fileURLToPath } from 'node:url';
24+
25+
const HERE = path.dirname(fileURLToPath(import.meta.url));
26+
const PKG = path.resolve(HERE, '..');
27+
const TSX = path.join(PKG, 'node_modules', '.bin', 'tsx');
28+
const GENERATOR = path.join(HERE, 'build-migration-registry.ts');
29+
const SPAWN_TIMEOUT_MS = 60_000;
30+
31+
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'build-migration-registry-entry-'));
32+
afterAll(() => fs.rmSync(scratch, { recursive: true, force: true }));
33+
34+
function tsx(args: string[]): { status: number; stdout: string; stderr: string } {
35+
const r = spawnSync(TSX, args, {
36+
cwd: PKG,
37+
encoding: 'utf8',
38+
timeout: SPAWN_TIMEOUT_MS,
39+
stdio: ['ignore', 'pipe', 'pipe'],
40+
});
41+
return { status: r.status ?? -1, stdout: r.stdout ?? '', stderr: r.stderr ?? '' };
42+
}
43+
44+
describe('build-migration-registry.ts — run, it speaks; import, it runs nothing', () => {
45+
it('a direct run still reaches the generator (its self-test answers)', { timeout: SPAWN_TIMEOUT_MS }, () => {
46+
const r = tsx([GENERATOR, '--self-test']);
47+
expect(r.status, r.stderr).toBe(0);
48+
expect(r.stdout).toContain('build-migration-registry --self-test: ok');
49+
});
50+
51+
it('a run through a symlink still reaches it — node keeps argv[1] as typed', { timeout: SPAWN_TIMEOUT_MS }, () => {
52+
const link = path.join(scratch, 'linked-generator.ts');
53+
fs.symlinkSync(GENERATOR, link);
54+
const r = tsx([link, '--self-test']);
55+
expect(r.status, r.stderr).toBe(0);
56+
expect(r.stdout).toContain('build-migration-registry --self-test: ok');
57+
});
58+
59+
it('an import runs nothing: the importer prints exactly what it asked for', { timeout: SPAWN_TIMEOUT_MS }, () => {
60+
// The README's own example, so a reader can check the expectation by eye.
61+
const probe = path.join(scratch, 'import-probe.ts');
62+
fs.writeFileSync(
63+
probe,
64+
`import { shardNameFor } from ${JSON.stringify(GENERATOR)};\n` +
65+
`console.log(shardNameFor(17, 'data/AggregationNode:distinct'));\n`,
66+
);
67+
const r = tsx([probe]);
68+
expect(r.status, r.stderr).toBe(0);
69+
// Write mode would have printed `✓ wrote src/migrations/registry.ts (…)`, and
70+
// check mode `✓ … is current` — any line beyond the probe's own is the defect.
71+
expect(r.stdout).toBe('17.data__AggregationNode__distinct.ts\n');
72+
expect(r.stderr).toBe('');
73+
});
74+
});

‎packages/spec/scripts/build-migration-registry.ts‎

Lines changed: 71 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@
9090
* pnpm --filter @objectstack/spec check:migration-registry # --self-test && --check
9191
*/
9292

93-
import { readFileSync, readdirSync, writeFileSync } from 'node:fs';
93+
import { readFileSync, readdirSync, realpathSync, writeFileSync } from 'node:fs';
9494
import { dirname, join } from 'node:path';
9595
import { fileURLToPath } from 'node:url';
9696

@@ -144,6 +144,10 @@ export interface Entry {
144144
* FUNCTION (one id → one name), never reversible — the id is written inside the
145145
* file, and two ids that collided on a name would collide as an add/add
146146
* conflict, which is the behaviour duplicates should get anyway.
147+
*
148+
* `build-schemas.ts` names the file each of its registration remedies asks for
149+
* through this same function, so a gate never prints a name this generator
150+
* would refuse.
147151
*/
148152
export function shardNameFor(major: number, id: string): string {
149153
return `${major}.${id.replaceAll('/', '__').replaceAll(':', '__')}.ts`;
@@ -423,58 +427,79 @@ function selfTest(): string[] {
423427
}
424428

425429
// ---------------------------------------------------------------------------
426-
// main
430+
// main — only when node RAN this file, never when something imported it
427431
// ---------------------------------------------------------------------------
432+
//
433+
// `build-schemas.ts` imports `shardNameFor`, `KINDS` and `ENTRIES_DIR`, so every
434+
// remedy it prints names the entry file THIS rule accepts — the one naming rule
435+
// `entries/README.md` documents, read rather than restated beside it. An import
436+
// must therefore run nothing: write mode rewrites `registry.ts`. Spelled as
437+
// `check-generated.ts` spells it, realpath on BOTH sides — node resolves
438+
// symlinks for the module graph but leaves `argv[1]` as typed, and a guard that
439+
// compared the two raw would turn `check:migration-registry` into exit 0 with
440+
// no output. `build-migration-registry-entry.test.ts` pins both directions.
441+
442+
function main(argv: readonly string[]): void {
443+
if (argv.includes('--self-test')) {
444+
const failures = selfTest();
445+
if (failures.length) {
446+
console.error('build-migration-registry --self-test FAILED:\n' + failures.map((f) => ` - ${f}`).join('\n'));
447+
process.exit(1);
448+
}
449+
console.log('build-migration-registry --self-test: ok');
450+
if (argv.length === 1) process.exit(0);
451+
}
428452

429-
const argv = process.argv.slice(2);
453+
const check = argv.includes('--check');
454+
const registryPath = join(pkgRoot, REGISTRY_PATH);
455+
const source = readFileSync(registryPath, 'utf8');
430456

431-
if (argv.includes('--self-test')) {
432-
const failures = selfTest();
433-
if (failures.length) {
434-
console.error('build-migration-registry --self-test FAILED:\n' + failures.map((f) => ` - ${f}`).join('\n'));
435-
process.exit(1);
457+
const byKind = {} as Record<Kind, Entry[]>;
458+
const errors: string[] = [];
459+
for (const { kind } of KINDS) {
460+
const loaded = loadEntries(kind);
461+
byKind[kind] = loaded.entries;
462+
errors.push(...loaded.errors);
436463
}
437-
console.log('build-migration-registry --self-test: ok');
438-
if (argv.length === 1) process.exit(0);
439-
}
440464

441-
const check = argv.includes('--check');
442-
const registryPath = join(pkgRoot, REGISTRY_PATH);
443-
const source = readFileSync(registryPath, 'utf8');
465+
const { text, errors: spliceErrors } = renderRegistry(source, byKind);
466+
errors.push(...spliceErrors);
444467

445-
const byKind = {} as Record<Kind, Entry[]>;
446-
const errors: string[] = [];
447-
for (const { kind } of KINDS) {
448-
const loaded = loadEntries(kind);
449-
byKind[kind] = loaded.entries;
450-
errors.push(...loaded.errors);
451-
}
452-
453-
const { text, errors: spliceErrors } = renderRegistry(source, byKind);
454-
errors.push(...spliceErrors);
468+
if (errors.length) {
469+
console.error('build-migration-registry: the entry directories are not readable as a registry.\n');
470+
for (const e of errors) console.error(` ✗ ${e}`);
471+
process.exit(1);
472+
}
455473

456-
if (errors.length) {
457-
console.error('build-migration-registry: the entry directories are not readable as a registry.\n');
458-
for (const e of errors) console.error(` ✗ ${e}`);
459-
process.exit(1);
474+
const counts = KINDS.map(({ kind }) => `${byKind[kind].length} ${kind}`).join(', ');
475+
476+
if (check) {
477+
if (text !== source) {
478+
console.error(
479+
`✗ ${REGISTRY_PATH} is stale — its generated regions do not match ${ENTRIES_DIR}/.\n\n` +
480+
' Run: pnpm --filter @objectstack/spec gen:migration-registry\n\n' +
481+
' If you reached this after resolving a merge conflict INSIDE a marked region: do not\n' +
482+
' hand-merge it. The entry files are the source and git merged them as a set; the only\n' +
483+
' correct resolution is to regenerate. This gate exists because a resolution that drops\n' +
484+
" one side's entry produces no other error anywhere (#6957).",
485+
);
486+
process.exit(1);
487+
}
488+
console.log(`✓ ${REGISTRY_PATH} is current (${counts})`);
489+
} else {
490+
if (text !== source) writeFileSync(registryPath, text);
491+
console.log(`✓ wrote ${REGISTRY_PATH} (${counts})`);
492+
}
460493
}
461494

462-
const counts = KINDS.map(({ kind }) => `${byKind[kind].length} ${kind}`).join(', ');
463-
464-
if (check) {
465-
if (text !== source) {
466-
console.error(
467-
`✗ ${REGISTRY_PATH} is stale — its generated regions do not match ${ENTRIES_DIR}/.\n\n` +
468-
' Run: pnpm --filter @objectstack/spec gen:migration-registry\n\n' +
469-
' If you reached this after resolving a merge conflict INSIDE a marked region: do not\n' +
470-
' hand-merge it. The entry files are the source and git merged them as a set; the only\n' +
471-
' correct resolution is to regenerate. This gate exists because a resolution that drops\n' +
472-
" one side's entry produces no other error anywhere (#6957).",
473-
);
474-
process.exit(1);
495+
const invokedDirectly = (() => {
496+
const entry = process.argv[1];
497+
if (!entry) return false;
498+
try {
499+
return realpathSync(entry) === realpathSync(fileURLToPath(import.meta.url));
500+
} catch {
501+
return false;
475502
}
476-
console.log(`✓ ${REGISTRY_PATH} is current (${counts})`);
477-
} else {
478-
if (text !== source) writeFileSync(registryPath, text);
479-
console.log(`✓ wrote ${REGISTRY_PATH} (${counts})`);
480-
}
503+
})();
504+
505+
if (invokedDirectly) main(process.argv.slice(2));

0 commit comments

Comments
 (0)