Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .changeset/cli-truncation-remainder-notices.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
---
'@objectstack/cli': patch
---

`os build`, `os validate` and `os init` say how many diagnostics they withheld

Nine more renders across the three authoring commands cut their list at a fixed
cap and printed nothing saying so — the `--strict-body` refusal list, the
author-time rule failures, the undeclared-authoring-key findings, the
access-matrix drift, the package-doc errors, and both halves of `os init`'s
scaffold self-test. The defect is the **silence**, not the cap: truncated
output that carries no notice is indistinguishable from complete output, so an
author who reads it and sees no further problems has read a list that stopped
early. Two of them even stated the true total in their own header and then
showed fewer rows, so the report gave two numbers that disagreed and explained
neither.

On the gating lists it also undoes the thing `os validate` went out of its way
to provide. Its own comment records why every failing rule reports at once:
"the command used to exit at the first failing gate, so an author with three
unrelated problems fixed them in three round trips and could not see how deep
the hole went". Past the cap that is exactly what came back, one cap-width at a
time, with each round of fixes revealing a new batch that reads as fresh
breakage.

Every cap stays. Over it the output now names the exact remainder:

```
⚠ … and 30 more author-time rule failure(s) not shown (50 of 80) — re-run with --json for the full list
```

**The pointer is verified per site, and two notices deliberately omit it.**
`--json` publishes each of these lists at the very exit whose text face carries
the notice, so re-running really does return the complete set. `os init`
declares no `--json` flag at all, so both of its notices state the remainder
and name no remedy — a notice whose remedy does not work is worse than a silent
cut, because it sends the author down a path that returns the same truncated
view.

At or under a cap, nothing new is printed and the rendering is byte-for-byte
what it was.
64 changes: 48 additions & 16 deletions packages/cli/src/commands/compile.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,6 +29,10 @@ import {
printStep,
printWarning,
printAuthoringAdvisories,
printAuthoringRuleErrors,
printDocIssueErrors,
printBulletList,
JSON_FULL_LIST_REMEDY,
createTimer,
formatZodErrors,
collectMetadataStats,
Expand DownExpand Up@@ -142,9 +146,17 @@ export default class Compile extends Command {
}
console.log('');
printError(`--strict-body: ${issues.length} callable(s) lack a metadata body`);
for (const w of issues.slice(0, 20)) {
console.log(` • ${w.origin}: ${w.reason}`);
}
// [#11642] Caps at 20, not 50, which is the only reason a sweep
// anchored on the literal `slice(0, 50)` could not see this one. The
// shape is the defect either way: the header states the true total
// and the body shows 20, with nothing saying the rest exist. The cap
// stays; the silence does not. The pointer is honest here — the
// `--json` branch immediately above this block publishes the whole
// list as `issues`.
printBulletList(
issues.map((w) => `${w.origin}: ${w.reason}`),
{ noun: 'callable(s)', limit: 20, remedy: JSON_FULL_LIST_REMEDY },
);
this.exit(1);
}
}
Expand DownExpand Up@@ -233,11 +245,9 @@ export default class Compile extends Command {
}
console.log('');
printError(`Author-time rules failed (${ruleErrors.length} issue${ruleErrors.length > 1 ? 's' : ''})`);
for (const f of ruleErrors.slice(0, 50)) {
console.log(` • ${f.where}: ${f.message}`);
console.log(chalk.dim(` ${f.hint}`));
console.log(chalk.dim(` rule: ${f.rule} at ${f.path}`));
}
// [#11642] `--json` on this same exit publishes every one of them as
// `issues`, so the pointer resolves to a complete view of THIS list.
printAuthoringRuleErrors(ruleErrors, { remedy: JSON_FULL_LIST_REMEDY });
this.exit(1);
}

Expand DownExpand Up@@ -303,9 +313,28 @@ export default class Compile extends Command {
].map(formatUnknownAuthoringKey);
if (unknownKeyWarnings.length > 0 && !flags.json) {
printWarning(`Undeclared authoring keys (${unknownKeyWarnings.length}) — dropped at load (#3786)`);
for (const w of unknownKeyWarnings.slice(0, 50)) {
console.log(` • ${w}`);
}
// [#11642] The header already states the true total, so before this
// notice the block printed two numbers that disagreed and explained
// neither. The pointer resolves because #11643 put this exact list
// into the `--json` payload (`warnings`) a few lines below; it would
// have been a dead end before that landed.
//
// ⚠️ …and it resolves ON THE SUCCESS EXIT ONLY — the one conditional
// pointer of the nine. `warnings` lives in the terminal payload, so a
// build that fails at a LATER gate (access matrix 3e, package docs 3f,
// the runtime bundle) emits that gate's failure payload instead, and
// none of those carries this list: the author is told to re-run with
// `--json` and gets a payload without the withheld keys in it. The six
// error-path notices have no such gap — their `--json` branch sits in
// the same block as the text face. Filed as #11772; closing it means
// changing a `--json` payload shape, which is a machine-contract
// decision and not this card's. ⛔ Do not read the line above as
// unconditional — an unqualified claim that holds in one branch is the
// same shape as the silence this whole change is about.
printBulletList(unknownKeyWarnings, {
noun: 'undeclared authoring key(s)',
remedy: JSON_FULL_LIST_REMEDY,
});
}

// 3e. [ADR-0090 D6] Access-matrix snapshot gate. Opt-in per app: when
Expand DownExpand Up@@ -343,7 +372,12 @@ export default class Compile extends Command {
}
console.log('');
printError(`Access matrix drift (${drift.length} change${drift.length > 1 ? 's' : ''}) — capability changes must be reviewed`);
for (const line of drift.slice(0, 50)) console.log(` • ${line}`);
// [#11642] `--json` on this same exit publishes the whole diff
// as `changes`, so the pointer resolves for this list too.
printBulletList(drift, {
noun: 'access-matrix change(s)',
remedy: JSON_FULL_LIST_REMEDY,
});
console.log(chalk.dim(' If intended, re-run with --update-access-matrix and commit the snapshot — its diff IS the review artifact.'));
this.exit(1);
}
Expand All@@ -370,10 +404,8 @@ export default class Compile extends Command {
}
console.log('');
printError(`Package docs validation failed (${docErrors.length} issue${docErrors.length > 1 ? 's' : ''})`);
for (const i of docErrors.slice(0, 50)) {
console.log(` • ${i.path}: ${i.message}`);
console.log(chalk.dim(` rule: ${i.rule}`));
}
// [#11642] `--json` on this same exit publishes them all as `issues`.
printDocIssueErrors(docErrors, { remedy: JSON_FULL_LIST_REMEDY });
this.exit(1);
}
if (docWarnings.length > 0 && !flags.json) {
Expand Down
40 changes: 29 additions & 11 deletions packages/cli/src/commands/init.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,18 @@ import chalk from 'chalk';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { printHeader, printSuccess, printError, printStep, printKV, printInfo, formatZodErrors } from '../utils/format.js';
import {
printHeader,
printSuccess,
printError,
printStep,
printKV,
printInfo,
formatZodErrors,
printAuthoringAdvisories,
printAuthoringRuleErrors,
AUTHORING_ADVISORY_PRINT_LIMIT,
} from '../utils/format.js';
import { validateScaffold } from '../utils/scaffold-validate.js';
import { summarizeTree, describeEntry } from 'create-objectstack/created-summary';

Expand DownExpand Up@@ -890,11 +901,17 @@ export default class Init extends Command {
try {
const report = await validateScaffold(targetDir);

for (const f of report.advisories.slice(0, 50)) {
printWarning(`${f.where}: ${f.message}`);
if (f.hint) console.log(chalk.dim(` ${f.hint}`));
console.log(chalk.dim(` rule: ${f.rule} at ${f.path}`));
}
// [#11642] The SAME printer `os build` renders its advisories with
// — this block was a byte-for-byte copy of it, cap included — so the
// two cannot drift, and the remainder now gets named here too.
//
// ⛔ …but with NO pointer, and that is the point of the third
// argument. `os build`'s notice ends "re-run with --json for the
// full list"; `os init` declares no `--json` flag at all (see
// `static override flags` above), so offering it here would name a
// remedy that does not exist and send the author to a dead end.
// Stating the remainder without a pointer is the honest form.
printAuthoringAdvisories(report.advisories, AUTHORING_ADVISORY_PRINT_LIMIT, null);

if (report.schemaError) {
printError('Scaffold validation failed: rendered config does not satisfy the protocol schema');
Expand All@@ -905,11 +922,12 @@ export default class Init extends Command {
printError(
`Scaffold validation failed: author-time rules rejected the generated project (${report.errors.length} issue${report.errors.length > 1 ? 's' : ''})`,
);
for (const f of report.errors.slice(0, 50)) {
console.log(` • ${f.where}: ${f.message}`);
if (f.hint) console.log(chalk.dim(` ${f.hint}`));
console.log(chalk.dim(` rule: ${f.rule} at ${f.path}`));
}
// [#11642] Same printer as `os build` / `os validate`, and again
// with no `--json` pointer — this command has no such flag. The
// remedy this site DOES have is the line printed below: a scaffold
// its own generator's rules reject is a CLI bug, so the action is
// to report it, not to read a longer list.
printAuthoringRuleErrors(report.errors, { remedy: null });
scaffoldRejected = true;
} else {
printSuccess(
Expand Down
20 changes: 11 additions & 9 deletions packages/cli/src/commands/validate.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,9 @@ import {
printSuccess,
printError,
printStep,
printAuthoringRuleErrors,
printDocIssueErrors,
JSON_FULL_LIST_REMEDY,
createTimer,
formatZodErrors,
collectMetadataStats,
Expand DownExpand Up@@ -137,11 +140,12 @@ export default class Validate extends Command {
}
console.log('');
printError(`Author-time rules failed (${ruleErrors.length} issue${ruleErrors.length > 1 ? 's' : ''})`);
for (const f of ruleErrors.slice(0, 50)) {
console.log(` • ${f.where}: ${f.message}`);
console.log(chalk.dim(` ${f.hint}`));
console.log(chalk.dim(` rule: ${f.rule} at ${f.path}`));
}
// [#11642] The comment above is the reason this render may not be
// silently capped: reporting every failing rule at once is the whole
// point of the block, and a cut with no notice restores a smaller
// version of the round-trip it removed. `--json` on this same exit
// publishes all of them as `errors`, so the pointer resolves.
printAuthoringRuleErrors(ruleErrors, { remedy: JSON_FULL_LIST_REMEDY });
this.exit(1);
}

Expand DownExpand Up@@ -209,10 +213,8 @@ export default class Validate extends Command {
}
console.log('');
printError(`Package docs validation failed (${docErrors.length} issue${docErrors.length > 1 ? 's' : ''})`);
for (const i of docErrors.slice(0, 50)) {
console.log(` • ${i.path}: ${i.message}`);
console.log(chalk.dim(` rule: ${i.rule}`));
}
// [#11642] `--json` on this same exit publishes them all as `errors`.
printDocIssueErrors(docErrors, { remedy: JSON_FULL_LIST_REMEDY });
this.exit(1);
}

Expand Down
Loading
Loading