@@ -13,6 +13,7 @@ import {
1313 MIGRATION_MAJORS ,
1414 MIGRATION_SUPPORT_FLOOR ,
1515 type MigrationChainResult ,
16+ type MigrationTodo ,
1617} from '@objectstack/spec/migrations' ;
1718import { PROTOCOL_MAJOR , PROTOCOL_VERSION } from '@objectstack/spec/kernel' ;
1819import { FILE_REFERENCE_TYPES , REFERENCE_VALUE_TYPES , STRUCTURED_JSON_TYPES } from '@objectstack/spec/data' ;
@@ -295,13 +296,79 @@ function printSchemaVerdict(report: MigrationReport): void {
295296 console . log ( '' ) ;
296297}
297298
299+ /**
300+ * The semantic entries that judge each conversion, keyed by conversion id —
301+ * read off the entries' declared `conversionIds` (`SemanticMigration`), in the
302+ * order the chain reports the entries.
303+ *
304+ * ⛔ Declared links only. An entry's prose naming a conversion id is not a
305+ * link: prose also names incidental analogues, so the spec lane authors a link
306+ * only after reading both sides, and this printer pairs nothing it was not
307+ * told to.
308+ *
309+ * The join runs over the whole chain result, not hop by hop: a link may name a
310+ * conversion an EARLIER step replays than the entry's own, and both halves sit
311+ * in the flat `applied` / `todos` arrays whichever hop produced them.
312+ */
313+ function judgesByConversion ( todos : readonly MigrationTodo [ ] ) : ReadonlyMap < string , readonly MigrationTodo [ ] > {
314+ const judges = new Map < string , MigrationTodo [ ] > ( ) ;
315+ for ( const todo of todos ) {
316+ for ( const conversionId of todo . conversionIds ?? [ ] ) {
317+ const list = judges . get ( conversionId ) ;
318+ if ( ! list ) judges . set ( conversionId , [ todo ] ) ;
319+ else if ( ! list . includes ( todo ) ) list . push ( todo ) ;
320+ }
321+ }
322+ return judges ;
323+ }
324+
325+ /**
326+ * Group ② — every applied edit, and beside the edits a semantic entry judges,
327+ * that entry's headline marked **review**.
328+ *
329+ * The edit lines are the chain's, one per edit, unchanged. A review line is
330+ * printed once under each RUN of consecutive edits by one conversion — the
331+ * chain replays one conversion at a time, so a conversion's edits arrive
332+ * together — and counts the edits above it that it judges. Once per run rather
333+ * than once per edit: a real upgrade wrote 13 edits by one judged conversion,
334+ * and repeating a paragraph-long judgment 13 times is the noise this report is
335+ * ordered to cut. Nothing is lost by it — every edit line still names its
336+ * conversion, and the review line says how many of the lines above it covers.
337+ *
338+ * The review line is a COPY of the entry's `[protocol N] surface → replacement`
339+ * headline, not the entry: ③ still prints that entry in full, in its place, so
340+ * its `why` and `verify` are found there by the same headline, and ③'s count
341+ * and lines do not change (ADR-0087 D3, "never silence").
342+ */
343+ function printAppliedEdits ( result : MigrationChainResult ) : void {
344+ const judges = judgesByConversion ( result . todos ) ;
345+ console . log ( chalk . bold ( ` Applied ${ result . applied . length } mechanical change(s):` ) ) ;
346+ let run = 0 ;
347+ for ( const [ i , a ] of result . applied . entries ( ) ) {
348+ console . log ( ` • ${ a . path } : ${ chalk . red ( a . from ) } → ${ chalk . green ( a . to ) } ${ chalk . dim ( `(${ a . conversionId } )` ) } ` ) ;
349+ run += 1 ;
350+ if ( result . applied [ i + 1 ] ?. conversionId === a . conversionId ) continue ; // the run goes on
351+ const subject = run === 1 ? 'the edit above' : `the ${ run } edits above` ;
352+ for ( const judge of judges . get ( a . conversionId ) ?? [ ] ) {
353+ console . log (
354+ ` ${ chalk . yellow ( '↳ review' ) } ${ subject } against the manual change `
355+ + `[protocol ${ judge . toMajor } ] ${ judge . surface } → ${ judge . replacement } ` ,
356+ ) ;
357+ }
358+ run = 0 ;
359+ }
360+ console . log ( '' ) ;
361+ }
362+
298363/**
299364 * The human report of an authored-source run, in the order an upgrader acts on
300365 * it, each group under one header line that counts it (ADR-0087 D3):
301366 *
302367 * ① the VERDICT and the REFUSALS — whether the migrated stack parses and, when
303368 * it does not, every refusal left after the chain: what still blocks it;
304- * ② the APPLIED mechanical edits — the diff the chain has already made;
369+ * ② the APPLIED mechanical edits — the diff the chain has already made, with
370+ * the semantic entry that judges an edit printed beside it, marked review
371+ * (see {@link printAppliedEdits});
305372 * ③ the SEMANTIC notices — every semantic entry of every hop crossed.
306373 *
307374 * ## Why this order
@@ -320,7 +387,8 @@ function printSchemaVerdict(report: MigrationReport): void {
320387 * silence": an entry may leave ③ only on a structured, stack-derived proof that
321388 * its surface is absent, and matching the prose of `surface` against the stack
322389 * is not one. So the groups MOVE and nothing else does: every line ② and ③
323- * printed before is printed after, byte-identical and in chain order.
390+ * printed before is printed after, byte-identical and in chain order. The one
391+ * addition is ②'s review lines, each a copy of an entry ③ still prints.
324392 * `--json` is untouched — its keys, its values and the order of its arrays.
325393 */
326394export function printMigrationReport ( report : MigrationReport ) : void {
@@ -352,14 +420,8 @@ export function printMigrationReport(report: MigrationReport): void {
352420 return ;
353421 }
354422
355- // ② The mechanical rewrites (auto-applied).
356- if ( result . applied . length > 0 ) {
357- console . log ( chalk . bold ( ` Applied ${ result . applied . length } mechanical change(s):` ) ) ;
358- for ( const a of result . applied ) {
359- console . log ( ` • ${ a . path } : ${ chalk . red ( a . from ) } → ${ chalk . green ( a . to ) } ${ chalk . dim ( `(${ a . conversionId } )` ) } ` ) ;
360- }
361- console . log ( '' ) ;
362- }
423+ // ② The mechanical rewrites (auto-applied), a judged edit's judge beside it.
424+ if ( result . applied . length > 0 ) printAppliedEdits ( result ) ;
363425
364426 // Per-hop checkpoints.
365427 if ( report . step ) {
0 commit comments