@metaobjectsdev/cli 1.0.2 → 1.0.4-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -5
- package/dist/src/commands/deps.d.ts +3 -0
- package/dist/src/commands/deps.d.ts.map +1 -0
- package/dist/src/commands/deps.js +200 -0
- package/dist/src/commands/deps.js.map +1 -0
- package/dist/src/commands/docs.d.ts.map +1 -1
- package/dist/src/commands/docs.js +19 -3
- package/dist/src/commands/docs.js.map +1 -1
- package/dist/src/commands/eject.d.ts +2 -1
- package/dist/src/commands/eject.d.ts.map +1 -1
- package/dist/src/commands/eject.js +235 -75
- package/dist/src/commands/eject.js.map +1 -1
- package/dist/src/commands/gen.d.ts.map +1 -1
- package/dist/src/commands/gen.js +173 -30
- package/dist/src/commands/gen.js.map +1 -1
- package/dist/src/commands/init.d.ts +5 -9
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +92 -402
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/migrate.d.ts.map +1 -1
- package/dist/src/commands/migrate.js +24 -19
- package/dist/src/commands/migrate.js.map +1 -1
- package/dist/src/commands/prompt-snapshot.d.ts.map +1 -1
- package/dist/src/commands/prompt-snapshot.js +2 -1
- package/dist/src/commands/prompt-snapshot.js.map +1 -1
- package/dist/src/commands/verify.d.ts.map +1 -1
- package/dist/src/commands/verify.js +186 -29
- package/dist/src/commands/verify.js.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +97 -15
- package/dist/src/index.js.map +1 -1
- package/dist/src/lib/advisory.d.ts +6 -0
- package/dist/src/lib/advisory.d.ts.map +1 -1
- package/dist/src/lib/advisory.js +10 -0
- package/dist/src/lib/advisory.js.map +1 -1
- package/dist/src/lib/agent-context-staleness.d.ts.map +1 -1
- package/dist/src/lib/agent-context-staleness.js +7 -1
- package/dist/src/lib/agent-context-staleness.js.map +1 -1
- package/dist/src/lib/args.d.ts +75 -3
- package/dist/src/lib/args.d.ts.map +1 -1
- package/dist/src/lib/args.js +54 -5
- package/dist/src/lib/args.js.map +1 -1
- package/dist/src/lib/catalog-listing.d.ts +92 -0
- package/dist/src/lib/catalog-listing.d.ts.map +1 -0
- package/dist/src/lib/catalog-listing.js +268 -0
- package/dist/src/lib/catalog-listing.js.map +1 -0
- package/dist/src/lib/catalog.d.ts +41 -0
- package/dist/src/lib/catalog.d.ts.map +1 -0
- package/dist/src/lib/catalog.js +89 -0
- package/dist/src/lib/catalog.js.map +1 -0
- package/dist/src/lib/collection-load-options.d.ts +13 -0
- package/dist/src/lib/collection-load-options.d.ts.map +1 -0
- package/dist/src/lib/collection-load-options.js +25 -0
- package/dist/src/lib/collection-load-options.js.map +1 -0
- package/dist/src/lib/dependency-sync.d.ts +170 -0
- package/dist/src/lib/dependency-sync.d.ts.map +1 -0
- package/dist/src/lib/dependency-sync.js +457 -0
- package/dist/src/lib/dependency-sync.js.map +1 -0
- package/dist/src/lib/install-set.d.ts +31 -0
- package/dist/src/lib/install-set.d.ts.map +1 -0
- package/dist/src/lib/install-set.js +103 -0
- package/dist/src/lib/install-set.js.map +1 -0
- package/dist/src/lib/library-eject.d.ts +79 -0
- package/dist/src/lib/library-eject.d.ts.map +1 -0
- package/dist/src/lib/library-eject.js +241 -0
- package/dist/src/lib/library-eject.js.map +1 -0
- package/dist/src/lib/library-listing.d.ts +63 -0
- package/dist/src/lib/library-listing.d.ts.map +1 -0
- package/dist/src/lib/library-listing.js +178 -0
- package/dist/src/lib/library-listing.js.map +1 -0
- package/dist/src/lib/library-prefix-advisory.d.ts +17 -0
- package/dist/src/lib/library-prefix-advisory.d.ts.map +1 -0
- package/dist/src/lib/library-prefix-advisory.js +67 -0
- package/dist/src/lib/library-prefix-advisory.js.map +1 -0
- package/dist/src/lib/load-metaobjects-config.d.ts +1 -2
- package/dist/src/lib/load-metaobjects-config.d.ts.map +1 -1
- package/dist/src/lib/load-metaobjects-config.js +11 -17
- package/dist/src/lib/load-metaobjects-config.js.map +1 -1
- package/dist/src/lib/migrate-scope.d.ts +32 -1
- package/dist/src/lib/migrate-scope.d.ts.map +1 -1
- package/dist/src/lib/migrate-scope.js +51 -3
- package/dist/src/lib/migrate-scope.js.map +1 -1
- package/dist/src/lib/overlay-lint.d.ts +37 -0
- package/dist/src/lib/overlay-lint.d.ts.map +1 -0
- package/dist/src/lib/overlay-lint.js +102 -0
- package/dist/src/lib/overlay-lint.js.map +1 -0
- package/dist/src/lib/requirement-check.d.ts +26 -3
- package/dist/src/lib/requirement-check.d.ts.map +1 -1
- package/dist/src/lib/requirement-check.js +71 -13
- package/dist/src/lib/requirement-check.js.map +1 -1
- package/package.json +10 -10
- package/src/commands/deps.ts +231 -0
- package/src/commands/docs.ts +19 -3
- package/src/commands/eject.ts +304 -90
- package/src/commands/gen.ts +189 -29
- package/src/commands/init.ts +95 -407
- package/src/commands/migrate.ts +26 -16
- package/src/commands/prompt-snapshot.ts +2 -1
- package/src/commands/verify.ts +213 -30
- package/src/index.ts +97 -15
- package/src/lib/advisory.ts +13 -0
- package/src/lib/agent-context-staleness.ts +7 -1
- package/src/lib/args.ts +118 -8
- package/src/lib/catalog-listing.ts +369 -0
- package/src/lib/catalog.ts +107 -0
- package/src/lib/collection-load-options.ts +42 -0
- package/src/lib/dependency-sync.ts +656 -0
- package/src/lib/install-set.ts +116 -0
- package/src/lib/library-eject.ts +298 -0
- package/src/lib/library-listing.ts +258 -0
- package/src/lib/library-prefix-advisory.ts +78 -0
- package/src/lib/load-metaobjects-config.ts +15 -21
- package/src/lib/migrate-scope.ts +59 -4
- package/src/lib/overlay-lint.ts +129 -0
- package/src/lib/requirement-check.ts +100 -14
package/src/commands/migrate.ts
CHANGED
|
@@ -13,7 +13,8 @@ import { buildKyselyFromUrl, redactUrl } from "../lib/kysely.js";
|
|
|
13
13
|
import { log } from "../lib/log.js";
|
|
14
14
|
import { loadMemory, resolveCollection, resolveConfigDir, type Collection } from "@metaobjectsdev/sdk";
|
|
15
15
|
import { loadMemoryOptionsFrom, loadMetaobjectsConfig, resolveGenConfigDir } from "../lib/load-metaobjects-config.js";
|
|
16
|
-
import {
|
|
16
|
+
import { collectionLoadOptions } from "../lib/collection-load-options.js";
|
|
17
|
+
import { exclusionNotes, importedOption, migrateScopeMismatch } from "../lib/migrate-scope.js";
|
|
17
18
|
import {
|
|
18
19
|
allowOptionFor,
|
|
19
20
|
buildExpectedSchemaWithProvenance,
|
|
@@ -201,7 +202,8 @@ function warnIfLedgerRelocated(cwd: string, resolvedOutDir: string): void {
|
|
|
201
202
|
}
|
|
202
203
|
|
|
203
204
|
/**
|
|
204
|
-
* Report what a declared `migrate.scope` left out
|
|
205
|
+
* Report what this run did not govern — what a declared `migrate.scope` left out, and
|
|
206
|
+
* what a DEPENDENCY owns (wording: `exclusionNotes`).
|
|
205
207
|
*
|
|
206
208
|
* STDOUT in text format, STDERR otherwise. `--format json` / `--format toon` put a
|
|
207
209
|
* single machine-readable document on stdout, and a prose line ahead of it breaks
|
|
@@ -210,11 +212,15 @@ function warnIfLedgerRelocated(cwd: string, resolvedOutDir: string): void {
|
|
|
210
212
|
* (`resolveFormat`): suppressing it outright would silence the note for every
|
|
211
213
|
* piped and CI run, which is most of them.
|
|
212
214
|
*/
|
|
213
|
-
function logOutOfScope(
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
215
|
+
function logOutOfScope(
|
|
216
|
+
names: readonly string[],
|
|
217
|
+
fromDependencies: readonly string[],
|
|
218
|
+
fmt: OutputFormat,
|
|
219
|
+
): void {
|
|
220
|
+
for (const msg of exclusionNotes("migrate", names, fromDependencies)) {
|
|
221
|
+
if (fmt === "text") log.info(msg);
|
|
222
|
+
else log.warn(msg);
|
|
223
|
+
}
|
|
218
224
|
}
|
|
219
225
|
|
|
220
226
|
function emitStructuredError(error: string, hint: string, fmt: OutputFormat): void {
|
|
@@ -629,7 +635,7 @@ export async function migrateCommand(
|
|
|
629
635
|
let metadata;
|
|
630
636
|
try {
|
|
631
637
|
metadata = await loadMemory(collection.configDir, {
|
|
632
|
-
|
|
638
|
+
...collectionLoadOptions(collection),
|
|
633
639
|
...postgresLoadOptions,
|
|
634
640
|
});
|
|
635
641
|
} catch (err) {
|
|
@@ -679,9 +685,9 @@ export async function migrateCommand(
|
|
|
679
685
|
// leave the expected schema here and are suppressed on the actual side below —
|
|
680
686
|
// dropping them from `expected` ALONE would propose DROP TABLE for every one of
|
|
681
687
|
// them that exists in the database.
|
|
682
|
-
const scoped = scopeExpectedSchema(built, collection.inMigrateScope);
|
|
688
|
+
const scoped = scopeExpectedSchema(built, collection.inMigrateScope, importedOption(collection));
|
|
683
689
|
const expected = scoped.snapshot;
|
|
684
|
-
logOutOfScope(scoped.outOfScope, fmt);
|
|
690
|
+
logOutOfScope(scoped.outOfScope, scoped.importedOutOfScope ?? [], fmt);
|
|
685
691
|
let actual;
|
|
686
692
|
try {
|
|
687
693
|
actual = await introspect(kysely.db, kysely.dialect);
|
|
@@ -1057,7 +1063,7 @@ export async function runBaseline(
|
|
|
1057
1063
|
try {
|
|
1058
1064
|
const collection = await resolveCollection(metaRoot);
|
|
1059
1065
|
metadata = await loadMemory(collection.configDir, {
|
|
1060
|
-
|
|
1066
|
+
...collectionLoadOptions(collection),
|
|
1061
1067
|
...baselineLoadOptions,
|
|
1062
1068
|
});
|
|
1063
1069
|
} catch (err) {
|
|
@@ -1196,7 +1202,7 @@ export async function runOfflineGenerate(
|
|
|
1196
1202
|
try {
|
|
1197
1203
|
collection = await resolveCollection(metaRoot);
|
|
1198
1204
|
metadata = await loadMemory(collection.configDir, {
|
|
1199
|
-
|
|
1205
|
+
...collectionLoadOptions(collection),
|
|
1200
1206
|
...offlineLoadOptions,
|
|
1201
1207
|
});
|
|
1202
1208
|
} catch (err) {
|
|
@@ -1256,6 +1262,10 @@ export async function runOfflineGenerate(
|
|
|
1256
1262
|
views: offlineViews,
|
|
1257
1263
|
// Per-command scope — narrows BOTH sides of the offline diff (see planOffline).
|
|
1258
1264
|
...(offlineScope !== undefined ? { inScope: offlineScope } : {}),
|
|
1265
|
+
// ...and the import exclusion, which planOffline threads into the SAME call.
|
|
1266
|
+
// Passing it only at the direct call sites would leave the offline pipeline —
|
|
1267
|
+
// the default `meta migrate` — performing no import exclusion at all.
|
|
1268
|
+
...importedOption(collection),
|
|
1259
1269
|
allow: tokensToAllowOptions(config.allow),
|
|
1260
1270
|
onAmbiguous: async (a) => {
|
|
1261
1271
|
collectedAmbiguous.push(a);
|
|
@@ -1280,7 +1290,7 @@ export async function runOfflineGenerate(
|
|
|
1280
1290
|
// entries); `expected` is the governed side the emitter renders against. Equal
|
|
1281
1291
|
// for an unscoped run.
|
|
1282
1292
|
const { diff: diffResult, nextSnapshot, expected: governedExpected } = plan;
|
|
1283
|
-
logOutOfScope(plan.outOfScope, fmt);
|
|
1293
|
+
logOutOfScope(plan.outOfScope, plan.importedOutOfScope ?? [], fmt);
|
|
1284
1294
|
|
|
1285
1295
|
if (diffResult.blocked.length > 0) {
|
|
1286
1296
|
log.error(`migrate: ${diffResult.blocked.length} destructive change(s) blocked; re-run with --allow <tokens>`);
|
|
@@ -1461,7 +1471,7 @@ async function runD1Migrate(
|
|
|
1461
1471
|
let metadata;
|
|
1462
1472
|
try {
|
|
1463
1473
|
metadata = await loadMemory(collection.configDir, {
|
|
1464
|
-
|
|
1474
|
+
...collectionLoadOptions(collection),
|
|
1465
1475
|
...d1LoadOptions,
|
|
1466
1476
|
});
|
|
1467
1477
|
} catch (err) {
|
|
@@ -1482,9 +1492,9 @@ async function runD1Migrate(
|
|
|
1482
1492
|
const scopeRc = refuseScopeMismatch(collection, () => built.provenance, fmt);
|
|
1483
1493
|
if (scopeRc !== undefined) return scopeRc;
|
|
1484
1494
|
// Per-command scope — both-sided, exactly as on the Kysely path above.
|
|
1485
|
-
const scoped = scopeExpectedSchema(built, collection.inMigrateScope);
|
|
1495
|
+
const scoped = scopeExpectedSchema(built, collection.inMigrateScope, importedOption(collection));
|
|
1486
1496
|
const expected = scoped.snapshot;
|
|
1487
|
-
logOutOfScope(scoped.outOfScope, fmt);
|
|
1497
|
+
logOutOfScope(scoped.outOfScope, scoped.importedOutOfScope ?? [], fmt);
|
|
1488
1498
|
let actual;
|
|
1489
1499
|
try {
|
|
1490
1500
|
actual = await introspectD1({
|
|
@@ -16,6 +16,7 @@ import { FileProvider } from "../lib/file-provider.js";
|
|
|
16
16
|
import { snapshotPaths, unifiedDiff } from "../lib/snapshot.js";
|
|
17
17
|
import { reportLoadError } from "../lib/load-error.js";
|
|
18
18
|
import { loadMemoryOptionsFrom, loadMetaobjectsConfig, resolveGenConfigDir } from "../lib/load-metaobjects-config.js";
|
|
19
|
+
import { collectionLoadOptions } from "../lib/collection-load-options.js";
|
|
19
20
|
import { loadMemory, resolveCollection } from "@metaobjectsdev/sdk";
|
|
20
21
|
import { TYPE_TEMPLATE, TEMPLATE_ATTR_TEXT_REF, TEMPLATE_ATTR_FORMAT } from "@metaobjectsdev/metadata";
|
|
21
22
|
import { render, ESCAPERS, type RenderFormat } from "@metaobjectsdev/render";
|
|
@@ -75,7 +76,7 @@ export async function promptSnapshotCommand(args: string[], cwd: string): Promis
|
|
|
75
76
|
let root;
|
|
76
77
|
try {
|
|
77
78
|
root = await loadMemory(collection.configDir, {
|
|
78
|
-
|
|
79
|
+
...collectionLoadOptions(collection),
|
|
79
80
|
...configLoadOptions,
|
|
80
81
|
});
|
|
81
82
|
} catch (err) {
|
package/src/commands/verify.ts
CHANGED
|
@@ -14,7 +14,9 @@ import { emitStructured, type OutputFormat } from "../lib/format.js";
|
|
|
14
14
|
import {
|
|
15
15
|
antiPatternRows, missingBaseUrlRows, removedPropRows, ranSection, skippedSection, warnCapped,
|
|
16
16
|
type AdvisoryDiagnosticRow, type AdvisoryFindingRow, type AdvisorySection,
|
|
17
|
+
libraryPrefixRows,
|
|
17
18
|
} from "../lib/advisory.js";
|
|
19
|
+
import { scanForUnprovenancedLibraryPrefix } from "../lib/library-prefix-advisory.js";
|
|
18
20
|
import { warnIfAgentContextStale } from "../lib/agent-context-staleness.js";
|
|
19
21
|
import { warnIfManifestIgnored } from "../lib/manifest-ignored-check.js";
|
|
20
22
|
import { scanSourceForAntiPatterns } from "../lib/anti-patterns.js";
|
|
@@ -24,12 +26,15 @@ import { replayRemedy } from "../lib/replay-remedy.js";
|
|
|
24
26
|
import { FileProvider } from "../lib/file-provider.js";
|
|
25
27
|
import { derivePayloadFieldTree } from "../lib/payload-field-tree.js";
|
|
26
28
|
import { loadMemoryOptionsFrom, loadMetaobjectsConfig, resolveGenCollection, resolveGenConfigDir } from "../lib/load-metaobjects-config.js";
|
|
29
|
+
import { collectionLoadOptions } from "../lib/collection-load-options.js";
|
|
27
30
|
import { computeCodegenDrift } from "../lib/codegen-drift.js";
|
|
28
31
|
import { computeDocsDrift } from "../lib/docs-drift.js";
|
|
29
32
|
import {
|
|
30
33
|
checkRequirements, summariseRequirements, scanRequirements, type Diagnostic,
|
|
31
34
|
} from "../lib/requirement-check.js";
|
|
32
35
|
import { lintRequirements } from "../lib/requirement-lint.js";
|
|
36
|
+
import { lintOverlays } from "../lib/overlay-lint.js";
|
|
37
|
+
import { FileSource } from "@metaobjectsdev/metadata/core";
|
|
33
38
|
import { resolveD1Config, resolveMigrateConfig } from "../lib/config.js";
|
|
34
39
|
import {
|
|
35
40
|
buildWranglerExecuteArgs,
|
|
@@ -69,8 +74,15 @@ import {
|
|
|
69
74
|
type D1Runner,
|
|
70
75
|
type DriftResult,
|
|
71
76
|
} from "@metaobjectsdev/migrate-ts";
|
|
72
|
-
import {
|
|
73
|
-
|
|
77
|
+
import {
|
|
78
|
+
DEFAULT_METAOBJECTS_DIR,
|
|
79
|
+
loadConfig,
|
|
80
|
+
loadMemory,
|
|
81
|
+
resolveCollection,
|
|
82
|
+
type DependencySpec,
|
|
83
|
+
} from "@metaobjectsdev/sdk";
|
|
84
|
+
import { checkDependencies, ERR_DEPENDENCY_UPSTREAM_DRIFT, formatCheckLine, readLockOrThrow } from "../lib/dependency-sync.js";
|
|
85
|
+
import { exclusionNotes, importedOption, migrateScopeMismatch } from "../lib/migrate-scope.js";
|
|
74
86
|
import {
|
|
75
87
|
TYPE_TEMPLATE,
|
|
76
88
|
TEMPLATE_SUBTYPE_PROMPT,
|
|
@@ -163,11 +175,15 @@ export async function verifyCommand(
|
|
|
163
175
|
// D1 has no URL connection); that check lives inside runSchemaVerify.
|
|
164
176
|
const runCodegen = flags.codegen;
|
|
165
177
|
const runDocs = flags.docs;
|
|
178
|
+
// Task 15 — same shape as --codegen/--docs: selected ONLY by its own flag,
|
|
179
|
+
// never folded into the bare-verify default (it needs the publisher
|
|
180
|
+
// reachable, which CI may not have — see the VerifyFlags doc on `deps`).
|
|
181
|
+
const runDeps = flags.deps;
|
|
166
182
|
if (!flags.anyExplicit) {
|
|
167
183
|
say(
|
|
168
184
|
"meta verify — running --templates (default). Explicit subverbs: " +
|
|
169
185
|
"--templates (prompt drift), --db/--dialect d1 (schema drift), --codegen (codegen drift), " +
|
|
170
|
-
"--docs (docs drift), " +
|
|
186
|
+
"--docs (docs drift), --deps (dependency drift), " +
|
|
171
187
|
"--replay/--replay-snapshot (the committed migration chain replays from empty).",
|
|
172
188
|
);
|
|
173
189
|
}
|
|
@@ -248,7 +264,7 @@ export async function verifyCommand(
|
|
|
248
264
|
let root: Awaited<ReturnType<typeof loadMemory>>;
|
|
249
265
|
try {
|
|
250
266
|
root = await loadMemory(collection.configDir, {
|
|
251
|
-
|
|
267
|
+
...collectionLoadOptions(collection),
|
|
252
268
|
...configLoadOptions,
|
|
253
269
|
strict: !flags.lax,
|
|
254
270
|
});
|
|
@@ -314,7 +330,7 @@ export async function verifyCommand(
|
|
|
314
330
|
const promptsDir = join(projectRoot, flags.prompts ?? DEFAULT_PROMPTS_DIR);
|
|
315
331
|
const provider = new FileProvider(promptsDir);
|
|
316
332
|
|
|
317
|
-
// The
|
|
333
|
+
// The advisory sections, captured as the gates run so the structured payload
|
|
318
334
|
// can carry them IN FULL. They were previously formatted straight to stderr and
|
|
319
335
|
// existed nowhere else, which is why 96% of a 239-finding report was unreachable
|
|
320
336
|
// by any flag, env var or format.
|
|
@@ -322,6 +338,12 @@ export async function verifyCommand(
|
|
|
322
338
|
skippedSection("the requirement pass did not run");
|
|
323
339
|
let antiPatternSection: AdvisorySection<AdvisoryFindingRow> =
|
|
324
340
|
skippedSection("the advisory anti-pattern pass did not run");
|
|
341
|
+
// FR-023 §11.1 item 4 (Task 17) — the overlay authoring lint. Its own section
|
|
342
|
+
// (never folded into `requirements`, which is about a different kind of node
|
|
343
|
+
// entirely) so the structured payload names what ran and what didn't the same
|
|
344
|
+
// way every other advisory pass does.
|
|
345
|
+
let overlaySection: AdvisorySection<AdvisoryDiagnosticRow> =
|
|
346
|
+
skippedSection("the overlay lint did not run");
|
|
325
347
|
// The ledger counts `meta verify` prints on every run. Undefined for a project
|
|
326
348
|
// declaring no requirement.* node at all (opt-in by declaration) — the payload
|
|
327
349
|
// then omits the block rather than reporting zeroes that would read as an empty
|
|
@@ -341,6 +363,7 @@ export async function verifyCommand(
|
|
|
341
363
|
const schemaExit = await runSchemaVerify();
|
|
342
364
|
const codegenExit = runCodegen ? await runCodegenVerify() : 0;
|
|
343
365
|
const docsExit = runDocs ? await runDocsVerify() : 0;
|
|
366
|
+
const depsExit = runDeps ? await runDepsVerify() : 0;
|
|
344
367
|
// Requirements have no subverb: `requirement.*` nodes are metadata, so they
|
|
345
368
|
// are checked on every `meta verify`. Opt-in by DECLARATION — a model with no
|
|
346
369
|
// requirement nodes is silent, not in drift.
|
|
@@ -351,17 +374,24 @@ export async function verifyCommand(
|
|
|
351
374
|
// would parse cleanly and do nothing at all.
|
|
352
375
|
const replayExit = flags.replay || flags.replaySnapshot ? await runReplayVerify() : 0;
|
|
353
376
|
|
|
377
|
+
// FR-023 §11.1 item 4 (Task 17) — the overlay authoring lint. Runs on every
|
|
378
|
+
// `meta verify`, not gated on any subverb (an unflagged cross-file
|
|
379
|
+
// redeclaration is a risk regardless of which drift gates were selected).
|
|
380
|
+
// Warnings ONLY — never changes the exit code.
|
|
381
|
+
await runOverlayLintAdvisory();
|
|
382
|
+
|
|
354
383
|
// Advisory verify-as-teacher pass: surface hand-rolled work the metadata could
|
|
355
384
|
// model. Warnings ONLY — never changes the exit code (bias to under-flagging).
|
|
356
385
|
// Suppressed with --no-antipatterns or META_NO_ANTIPATTERNS=1 for the rare
|
|
357
386
|
// noisy project (both opt-outs work on `meta verify` and `meta gen`).
|
|
358
|
-
runAntiPatternAdvisory();
|
|
387
|
+
await runAntiPatternAdvisory();
|
|
359
388
|
|
|
360
389
|
const exitCode = Math.max(
|
|
361
390
|
templateExit,
|
|
362
391
|
schemaExit,
|
|
363
392
|
codegenExit,
|
|
364
393
|
docsExit,
|
|
394
|
+
depsExit,
|
|
365
395
|
requirementExit,
|
|
366
396
|
replayExit,
|
|
367
397
|
);
|
|
@@ -378,6 +408,7 @@ export async function verifyCommand(
|
|
|
378
408
|
{ gate: "schema", ran: ranSchemaGate, ok: schemaExit === 0 },
|
|
379
409
|
{ gate: "codegen", ran: runCodegen, ok: codegenExit === 0 },
|
|
380
410
|
{ gate: "docs", ran: runDocs, ok: docsExit === 0 },
|
|
411
|
+
{ gate: "deps", ran: runDeps, ok: depsExit === 0 },
|
|
381
412
|
{ gate: "requirements", ran: true, ok: requirementExit === 0 },
|
|
382
413
|
{ gate: "replay", ran: flags.replay || flags.replaySnapshot, ok: replayExit === 0 },
|
|
383
414
|
],
|
|
@@ -385,6 +416,7 @@ export async function verifyCommand(
|
|
|
385
416
|
requirements: requirementSection,
|
|
386
417
|
requirementCounts,
|
|
387
418
|
antiPatterns: antiPatternSection,
|
|
419
|
+
overlays: overlaySection,
|
|
388
420
|
}),
|
|
389
421
|
fmt,
|
|
390
422
|
);
|
|
@@ -553,7 +585,7 @@ export async function verifyCommand(
|
|
|
553
585
|
columnNamingStrategy: viewStrategy,
|
|
554
586
|
views: buildProjectionViews(root, { dialect, columnNamingStrategy: viewStrategy }),
|
|
555
587
|
});
|
|
556
|
-
governed = scopeExpectedSchema(built, schemaScope);
|
|
588
|
+
governed = scopeExpectedSchema(built, schemaScope, importedOption(collection));
|
|
557
589
|
}
|
|
558
590
|
|
|
559
591
|
// `verifyReplay` calls `applyPending` itself. That is NOT a second replay: the
|
|
@@ -594,7 +626,7 @@ export async function verifyCommand(
|
|
|
594
626
|
// ONE scan for all three passes. The gate and the summary each used to walk the
|
|
595
627
|
// model AND resolve every @implementedBy claim for themselves — the resolution
|
|
596
628
|
// being the expensive half — and the lint added a third walk on top.
|
|
597
|
-
const scan = scanRequirements(root);
|
|
629
|
+
const scan = scanRequirements(root, { coverable: collection.inScope });
|
|
598
630
|
const diags = [...checkRequirements(root, scan)];
|
|
599
631
|
|
|
600
632
|
// Printed on EVERY run, clean or not — a gate that says nothing when it
|
|
@@ -616,8 +648,20 @@ export async function verifyCommand(
|
|
|
616
648
|
say(
|
|
617
649
|
`meta verify — requirements: ${s.total} entries (${s.functional} functional, ` +
|
|
618
650
|
`${s.architectural} architectural) — ${parts.join(", ")}; ` +
|
|
619
|
-
|
|
620
|
-
|
|
651
|
+
// FR-043 §5.4 — a project whose every requirement came from a shipped library
|
|
652
|
+
// is not measured, and says so rather than printing a ratio. Silence would be
|
|
653
|
+
// worse than either: a missing clause reads as a clean coverage result.
|
|
654
|
+
(s.entitiesTotal === undefined
|
|
655
|
+
? `coverage: not measured (no project-authored requirements).`
|
|
656
|
+
: `${s.entitiesClaimed}/${s.entitiesTotal} entities claimed, ` +
|
|
657
|
+
`counted over ${collection.files.length} metadata file(s)` +
|
|
658
|
+
// FR-023 — only when there ARE dependencies. A project that declares none
|
|
659
|
+
// must print the sentence it printed before dependencies existed, to the
|
|
660
|
+
// byte: "0 from dependencies" is noise on every existing project, and this
|
|
661
|
+
// line is a SURFACE the no-dependency guarantee covers like any other.
|
|
662
|
+
(collection.dependencies.length > 0
|
|
663
|
+
? `, ${collection.dependencies.length} from dependencies.`
|
|
664
|
+
: `.`)),
|
|
621
665
|
);
|
|
622
666
|
if (s.undecided > 0) {
|
|
623
667
|
say(
|
|
@@ -637,26 +681,25 @@ export async function verifyCommand(
|
|
|
637
681
|
.filter((k) => (s.byStatus[k] ?? 0) > 0)
|
|
638
682
|
.map((k) => ({ status: k, count: s.byStatus[k] ?? 0 })),
|
|
639
683
|
undecided: s.undecided,
|
|
640
|
-
|
|
641
|
-
|
|
684
|
+
// Absent when coverage was not measured (FR-043 §5.4), never zeroed: a reader
|
|
685
|
+
// cannot tell `0/0 claimed` from "not measured", and the two mean opposite
|
|
686
|
+
// things.
|
|
687
|
+
...(s.entitiesTotal === undefined
|
|
688
|
+
? {}
|
|
689
|
+
: { entitiesClaimed: s.entitiesClaimed ?? 0, entitiesTotal: s.entitiesTotal }),
|
|
642
690
|
metadataFiles: collection.files.length,
|
|
643
691
|
};
|
|
644
692
|
}
|
|
645
693
|
|
|
646
694
|
const errors = diags.filter((d) => d.severity === "error");
|
|
647
695
|
const warns = diags.filter((d) => d.severity === "warn");
|
|
648
|
-
// Named `fmtDiag`, not `fmt`: `fmt` is this command's OUTPUT FORMAT parameter,
|
|
649
|
-
// and a shadow of it inside the one function that must not confuse the two is
|
|
650
|
-
// how a structured run quietly reverts to text.
|
|
651
|
-
const fmtDiag = (d: Diagnostic): string =>
|
|
652
|
-
` ${d.code}${d.path !== undefined ? ` [${d.path}]` : ""}: ${d.message}`;
|
|
653
696
|
// Capped per SECTION, never across them: a ledger of a few hundred entries can
|
|
654
697
|
// produce hundreds of prose findings, and a shared budget would let the advisory
|
|
655
698
|
// lint push every gate warning off the end. The cap VALUE is now one shared
|
|
656
699
|
// constant (`--limit`), so raising it cannot miss a section — errors stay
|
|
657
700
|
// uncapped, as they always were.
|
|
658
|
-
for (const d of errors) log.error(
|
|
659
|
-
warnCapped(warns.map(
|
|
701
|
+
for (const d of errors) log.error(formatDiagnostic(d));
|
|
702
|
+
warnCapped(warns.map(formatDiagnostic), flags.limit, { structured });
|
|
660
703
|
|
|
661
704
|
// -- the authoring lint: its own section, its own cap ----------------------
|
|
662
705
|
// Separate from the gate above because it makes a different claim. The gate
|
|
@@ -685,7 +728,7 @@ export async function verifyCommand(
|
|
|
685
728
|
`meta verify — requirements: ${lint.length} authoring warning(s) ` +
|
|
686
729
|
`(advisory — does not fail the build):`,
|
|
687
730
|
);
|
|
688
|
-
warnCapped(lint.map(
|
|
731
|
+
warnCapped(lint.map(formatDiagnostic), flags.limit, { structured });
|
|
689
732
|
}
|
|
690
733
|
|
|
691
734
|
// Everything this pass found, uncapped, for the structured payload — gate
|
|
@@ -703,11 +746,49 @@ export async function verifyCommand(
|
|
|
703
746
|
return 0;
|
|
704
747
|
}
|
|
705
748
|
|
|
749
|
+
// -- overlay authoring lint (FR-023 §11.1 item 4, Task 17) ------------------
|
|
750
|
+
// Its own section, its own cap — same discipline as the requirement lint
|
|
751
|
+
// above: a noisy section must not push another section's findings off the
|
|
752
|
+
// end of a capped run. This lint has NO gate half at all (no
|
|
753
|
+
// "ledger disagrees with the model" claim to make), so nothing here can
|
|
754
|
+
// ever reach the exit code — records its result either way, same as the
|
|
755
|
+
// anti-pattern pass below.
|
|
756
|
+
//
|
|
757
|
+
// Reads raw file content (via `lintOverlays` + `declaredTopLevelKeys`), never
|
|
758
|
+
// the loaded/merged model: the merge has already lost which FILE contributed
|
|
759
|
+
// which declaration, and that is exactly what this lint needs to name.
|
|
760
|
+
async function runOverlayLintAdvisory(): Promise<void> {
|
|
761
|
+
if (flags.noOverlayLint) {
|
|
762
|
+
overlaySection = skippedSection("suppressed by --no-overlay-lint");
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
if (process.env.META_NO_OVERLAY_LINT === "1") {
|
|
766
|
+
overlaySection = skippedSection("suppressed by META_NO_OVERLAY_LINT=1");
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
let findings: Diagnostic[];
|
|
770
|
+
try {
|
|
771
|
+
findings = await lintOverlays(collection, (path) => new FileSource(path));
|
|
772
|
+
} catch (err) {
|
|
773
|
+
// Never let an advisory scan break verify — and never report it as clean.
|
|
774
|
+
overlaySection = skippedSection(`the overlay lint failed: ${(err as Error).message}`);
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
overlaySection = ranSection(findings.map((d) => toDiagnosticRow(d, "lint")));
|
|
778
|
+
if (findings.length > 0) {
|
|
779
|
+
log.warn(
|
|
780
|
+
`meta verify — overlays: ${findings.length} unflagged cross-file redeclaration(s) ` +
|
|
781
|
+
`(advisory — does not fail the build):`,
|
|
782
|
+
);
|
|
783
|
+
warnCapped(findings.map(formatDiagnostic), flags.limit, { structured });
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
706
787
|
// -- verify-as-teacher (advisory) ------------------------------------------
|
|
707
788
|
// Records its result EITHER WAY — a skip carries its reason rather than looking
|
|
708
789
|
// like a clean scan. Warnings only; nothing here reaches the exit code (the
|
|
709
790
|
// scanner's own header: bias to under-flagging, never a non-zero exit).
|
|
710
|
-
function runAntiPatternAdvisory(): void {
|
|
791
|
+
async function runAntiPatternAdvisory(): Promise<void> {
|
|
711
792
|
if (flags.noAntipatterns) {
|
|
712
793
|
antiPatternSection = skippedSection("suppressed by --no-antipatterns");
|
|
713
794
|
return;
|
|
@@ -746,10 +827,23 @@ export async function verifyCommand(
|
|
|
746
827
|
// advisory only — never breaks verify.
|
|
747
828
|
}
|
|
748
829
|
|
|
830
|
+
// FR-043 §3.5 — `metaobjects::` in this project's own metadata with no ejection
|
|
831
|
+
// provenance. Advisory because with the library NOT opted in nothing is broken; the
|
|
832
|
+
// opted-in case is refused at load instead.
|
|
833
|
+
let libraryPrefix: Awaited<ReturnType<typeof scanForUnprovenancedLibraryPrefix>> = [];
|
|
834
|
+
try {
|
|
835
|
+
libraryPrefix = await scanForUnprovenancedLibraryPrefix(
|
|
836
|
+
root, collection.ownFiles, collection.configDir,
|
|
837
|
+
);
|
|
838
|
+
} catch {
|
|
839
|
+
// Same discipline as its siblings: an advisory scan never breaks verify.
|
|
840
|
+
}
|
|
841
|
+
|
|
749
842
|
antiPatternSection = ranSection([
|
|
750
843
|
...antiPatternRows(findings),
|
|
751
844
|
...missingBaseUrlRows(baseUrl),
|
|
752
845
|
...removedPropRows(removedProps),
|
|
846
|
+
...libraryPrefixRows(libraryPrefix),
|
|
753
847
|
]);
|
|
754
848
|
if (findings.length > 0) {
|
|
755
849
|
log.warn(
|
|
@@ -772,6 +866,13 @@ export async function verifyCommand(
|
|
|
772
866
|
);
|
|
773
867
|
warnCapped(removedProps.map((f) => ` ${f.message}`), flags.limit, { structured });
|
|
774
868
|
}
|
|
869
|
+
if (libraryPrefix.length > 0) {
|
|
870
|
+
log.warn(
|
|
871
|
+
`meta verify — ${libraryPrefix.length} node(s) declared under "metaobjects::" with ` +
|
|
872
|
+
`no ejection provenance (advisory — does not fail the build):`,
|
|
873
|
+
);
|
|
874
|
+
warnCapped(libraryPrefix.map((f) => ` ${f.message}`), flags.limit, { structured });
|
|
875
|
+
}
|
|
775
876
|
}
|
|
776
877
|
|
|
777
878
|
// -- template (prompt / output) drift --------------------------------------
|
|
@@ -994,6 +1095,9 @@ export async function verifyCommand(
|
|
|
994
1095
|
allow,
|
|
995
1096
|
views: expectedViews,
|
|
996
1097
|
...(schemaScope !== undefined ? { inScope: schemaScope } : {}),
|
|
1098
|
+
// The import exclusion reaches scopeExpectedSchema through this option bag —
|
|
1099
|
+
// there is no direct call on this path to attach it to.
|
|
1100
|
+
...importedOption(collection),
|
|
997
1101
|
});
|
|
998
1102
|
} catch (err) {
|
|
999
1103
|
log.error(`verify: failed to introspect ${kysely.displayUrl}: ${(err as Error).message}`);
|
|
@@ -1080,6 +1184,8 @@ export async function verifyCommand(
|
|
|
1080
1184
|
allow,
|
|
1081
1185
|
views: expectedViews,
|
|
1082
1186
|
...(schemaScope !== undefined ? { inScope: schemaScope } : {}),
|
|
1187
|
+
// Same option bag on the D1 path, for the same reason.
|
|
1188
|
+
...importedOption(collection),
|
|
1083
1189
|
});
|
|
1084
1190
|
} catch (err) {
|
|
1085
1191
|
log.error(`verify: ${(err as Error).message}`);
|
|
@@ -1192,11 +1298,12 @@ export async function verifyCommand(
|
|
|
1192
1298
|
);
|
|
1193
1299
|
}
|
|
1194
1300
|
|
|
1195
|
-
// Same reasoning for the per-command scope: an object
|
|
1196
|
-
// was NOT checked, and silence would misreport it as
|
|
1197
|
-
// wording with `meta migrate` — one declaration, one
|
|
1198
|
-
|
|
1199
|
-
|
|
1301
|
+
// Same reasoning for the per-command scope and for imported metadata: an object
|
|
1302
|
+
// excluded either way was NOT checked, and silence would misreport it as
|
|
1303
|
+
// checked-and-clean. Shared wording with `meta migrate` — one declaration, one
|
|
1304
|
+
// sentence about it — and each object named once (`exclusionNotes`).
|
|
1305
|
+
for (const note of exclusionNotes("verify", driftResult.outOfScope, driftResult.importedOutOfScope ?? [])) {
|
|
1306
|
+
say(note);
|
|
1200
1307
|
}
|
|
1201
1308
|
|
|
1202
1309
|
const changes = driftResult.changes;
|
|
@@ -1249,7 +1356,7 @@ export async function verifyCommand(
|
|
|
1249
1356
|
if (genCollection !== collection) {
|
|
1250
1357
|
try {
|
|
1251
1358
|
codegenRoot = await loadMemory(genCollection.configDir, {
|
|
1252
|
-
|
|
1359
|
+
...collectionLoadOptions(genCollection),
|
|
1253
1360
|
...configLoadOptions,
|
|
1254
1361
|
strict: !flags.lax,
|
|
1255
1362
|
});
|
|
@@ -1390,6 +1497,55 @@ export async function verifyCommand(
|
|
|
1390
1497
|
}
|
|
1391
1498
|
return 1;
|
|
1392
1499
|
}
|
|
1500
|
+
|
|
1501
|
+
// -- dependency drift (Task 15, FR-023) --------------------------------------
|
|
1502
|
+
// Gated on --deps. Re-resolves each declared dependency exactly as `meta deps
|
|
1503
|
+
// check` does — never part of the bare-verify default (it needs the publisher
|
|
1504
|
+
// reachable, which CI may not have).
|
|
1505
|
+
async function runDepsVerify(): Promise<number> {
|
|
1506
|
+
// Read RAW declared specs (never `collection.dependencies`, which is the
|
|
1507
|
+
// already-`ResolvedDependency[]` the LOCK produced — it carries no
|
|
1508
|
+
// transport info, so it cannot be re-resolved). A project with no
|
|
1509
|
+
// config.json at all declares no dependencies.
|
|
1510
|
+
let depSpecs: readonly DependencySpec[] = [];
|
|
1511
|
+
try {
|
|
1512
|
+
const cfg = await loadConfig(join(collection.configDir, DEFAULT_METAOBJECTS_DIR));
|
|
1513
|
+
depSpecs = cfg.dependencies;
|
|
1514
|
+
} catch {
|
|
1515
|
+
depSpecs = [];
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
if (depSpecs.length === 0) {
|
|
1519
|
+
say("verify --deps: no dependencies declared — nothing to check.");
|
|
1520
|
+
return 0;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
let lock: Awaited<ReturnType<typeof readLockOrThrow>>;
|
|
1524
|
+
try {
|
|
1525
|
+
lock = await readLockOrThrow(collection.configDir);
|
|
1526
|
+
} catch (err) {
|
|
1527
|
+
log.error(`verify --deps: ${(err as Error).message}`);
|
|
1528
|
+
return 2;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
const results = await checkDependencies(collection.configDir, depSpecs, lock);
|
|
1532
|
+
const failing = results.filter((r) => r.status !== "current");
|
|
1533
|
+
|
|
1534
|
+
for (const r of results) {
|
|
1535
|
+
if (r.status === "current") say(formatCheckLine(r));
|
|
1536
|
+
else log.error(formatCheckLine(r));
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
if (failing.length > 0) {
|
|
1540
|
+
log.error(
|
|
1541
|
+
`verify --deps: ${failing.length} of ${results.length} dependenc` +
|
|
1542
|
+
`${results.length === 1 ? "y" : "ies"} drifted or unresolved (${ERR_DEPENDENCY_UPSTREAM_DRIFT}).`,
|
|
1543
|
+
);
|
|
1544
|
+
return 1;
|
|
1545
|
+
}
|
|
1546
|
+
say("verify --deps: every dependency's installed artifact matches the lock.");
|
|
1547
|
+
return 0;
|
|
1548
|
+
}
|
|
1393
1549
|
}
|
|
1394
1550
|
|
|
1395
1551
|
/**
|
|
@@ -1447,8 +1603,9 @@ interface RequirementCounts {
|
|
|
1447
1603
|
architectural: number;
|
|
1448
1604
|
byStatus: { status: string; count: number }[];
|
|
1449
1605
|
undecided: number;
|
|
1450
|
-
|
|
1451
|
-
|
|
1606
|
+
/** Absent when coverage was not measured — FR-043 §5.4. */
|
|
1607
|
+
entitiesClaimed?: number;
|
|
1608
|
+
entitiesTotal?: number;
|
|
1452
1609
|
/** How many metadata files the two entity counts were taken over. */
|
|
1453
1610
|
metadataFiles: number;
|
|
1454
1611
|
}
|
|
@@ -1460,6 +1617,17 @@ interface VerifyGateRow {
|
|
|
1460
1617
|
ok: boolean;
|
|
1461
1618
|
}
|
|
1462
1619
|
|
|
1620
|
+
/**
|
|
1621
|
+
* Format one `Diagnostic` as a printed TEXT line. Named `formatDiagnostic`, not
|
|
1622
|
+
* `fmt`: `fmt` is this command's OUTPUT FORMAT parameter, and a shadow of it
|
|
1623
|
+
* anywhere near this code is how a structured run quietly reverts to text.
|
|
1624
|
+
* Shared by the requirement gate/lint AND the overlay lint — every advisory or
|
|
1625
|
+
* gate pass that speaks in `Diagnostic` prints it identically.
|
|
1626
|
+
*/
|
|
1627
|
+
function formatDiagnostic(d: Diagnostic): string {
|
|
1628
|
+
return ` ${d.code}${d.path !== undefined ? ` [${d.path}]` : ""}: ${d.message}`;
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1463
1631
|
/** Project a requirement diagnostic into a payload row. */
|
|
1464
1632
|
function toDiagnosticRow(d: Diagnostic, source: "gate" | "lint"): AdvisoryDiagnosticRow {
|
|
1465
1633
|
return {
|
|
@@ -1491,6 +1659,7 @@ function buildVerifyPayload(input: {
|
|
|
1491
1659
|
requirements: AdvisorySection<AdvisoryDiagnosticRow>;
|
|
1492
1660
|
requirementCounts: RequirementCounts | undefined;
|
|
1493
1661
|
antiPatterns: AdvisorySection<AdvisoryFindingRow>;
|
|
1662
|
+
overlays: AdvisorySection<AdvisoryDiagnosticRow>;
|
|
1494
1663
|
}): Record<string, unknown> {
|
|
1495
1664
|
const ran = input.gates.filter((g) => g.ran);
|
|
1496
1665
|
const failed = ran.filter((g) => !g.ok);
|
|
@@ -1505,6 +1674,9 @@ function buildVerifyPayload(input: {
|
|
|
1505
1674
|
if (input.requirements.total > 0) {
|
|
1506
1675
|
parts.push(`${input.requirements.total} requirement diagnostic(s)`);
|
|
1507
1676
|
}
|
|
1677
|
+
if (input.overlays.status === "ran" && input.overlays.total > 0) {
|
|
1678
|
+
parts.push(`${input.overlays.total} overlay authoring finding(s)`);
|
|
1679
|
+
}
|
|
1508
1680
|
|
|
1509
1681
|
const help: string[] = [];
|
|
1510
1682
|
if (failed.length > 0) {
|
|
@@ -1517,7 +1689,17 @@ function buildVerifyPayload(input: {
|
|
|
1517
1689
|
`${input.antiPatterns.total} authored site(s) hand-roll what MetaObjects can model — see antiPatterns.rows[] and run \`meta types <construct>\``,
|
|
1518
1690
|
);
|
|
1519
1691
|
}
|
|
1520
|
-
if (
|
|
1692
|
+
if (input.overlays.total > 0) {
|
|
1693
|
+
help.push(
|
|
1694
|
+
`${input.overlays.total} unflagged cross-file redeclaration(s) — see overlays.rows[]; add overlay: true so a renamed or removed target fails loudly instead of silently becoming a new object`,
|
|
1695
|
+
);
|
|
1696
|
+
}
|
|
1697
|
+
if (
|
|
1698
|
+
failed.length === 0 &&
|
|
1699
|
+
input.antiPatterns.total === 0 &&
|
|
1700
|
+
input.requirements.total === 0 &&
|
|
1701
|
+
input.overlays.total === 0
|
|
1702
|
+
) {
|
|
1521
1703
|
help.push("no drift and nothing advisory to answer — nothing to do");
|
|
1522
1704
|
}
|
|
1523
1705
|
|
|
@@ -1528,6 +1710,7 @@ function buildVerifyPayload(input: {
|
|
|
1528
1710
|
help,
|
|
1529
1711
|
antiPatterns: input.antiPatterns,
|
|
1530
1712
|
requirements: input.requirements,
|
|
1713
|
+
overlays: input.overlays,
|
|
1531
1714
|
...(input.requirementCounts !== undefined ? { requirementCounts: input.requirementCounts } : {}),
|
|
1532
1715
|
// The honest boundary. Everything named here is REACHABLE — it is printed as
|
|
1533
1716
|
// text on stderr — but it is not in this document, and a reader must not have
|