@@ -38,6 +38,32 @@ import {
3838
3939const objStack = ( obj : Record < string , unknown > ) => ( { objects : [ { name : 'widget' , ...obj } ] } ) ;
4040const paths = ( findings : { message : string } [ ] ) => findings . map ( ( f ) => f . message ) ;
41+
42+ /**
43+ * The shipped `translation` ledger warns on no group since `flows` went `live`
44+ * (both halves read by the console's screen-flow runner), so every pin below
45+ * that needs a WARNED translation group plants one: a copy of the shipped
46+ * ledger directory whose `translation.json` gains a single synthetic
47+ * `planned` + `authorWarn` group row, read through the same
48+ * `lintLivenessPropertiesFromLedgerDir` seam the synthetic `field` ledger
49+ * further down uses. What those pins hold is the locale-keyed WALK and the
50+ * planned verdict's wording, not any shipped verdict; the shipped `flows` row
51+ * is pinned SILENT on the real ledger beside them.
52+ */
53+ const SYNTH_WARNED_GROUP = 'synthWarnedGroup' ;
54+ const SYNTH_WARNED_GROUP_HINT = 'synthetic — this group row exists only in a test ledger directory' ;
55+ const synthWarnedGroup = { probe : { label : '探针' } } ;
56+ const plantedTranslationLedgerDir = ( ) => ledgerDirWith ( ( dir ) => {
57+ const ledger = JSON . parse ( readFileSync ( join ( dir , 'translation.json' ) , 'utf8' ) ) ;
58+ ledger . props [ SYNTH_WARNED_GROUP ] = {
59+ status : 'planned' ,
60+ authorWarn : true ,
61+ authorHint : SYNTH_WARNED_GROUP_HINT ,
62+ } ;
63+ writeLedger ( dir , 'translation' , ledger ) ;
64+ } ) ;
65+ const lintWithPlantedGroup = ( stack : Record < string , unknown > ) =>
66+ lintLivenessPropertiesFromLedgerDir ( plantedTranslationLedgerDir ( ) , stack ) ;
4167/** The rule id of the finding about `path`, or `undefined` when the rule said nothing about it. */
4268const ruleOf = ( findings : { message : string ; rule : string } [ ] , path : string ) =>
4369 findings . find ( ( f ) => f . message . includes ( `sets \`${ path } \`` ) ) ?. rule ;
@@ -616,7 +642,9 @@ describe('lintLivenessProperties', () => {
616642 // (That enumeration is dated to its own commit and has since lost a member:
617643 // `field.relatedListFilter` left the warned set in #19187 when objectui#4664
618644 // reached the pin. The claim it supports — every warned entry is TOP-LEVEL, so
619- // no dotted subject is left — is unchanged by a top-level row leaving.)
645+ // no dotted subject is left — is unchanged by a top-level row leaving.
646+ // `translation.flows` has left it too, by the same route: it went `live` once
647+ // the runner reading the flow's own `label` reached the pin.)
620648 // Filed as **#10262**, which also carries the recommendation not to play this
621649 // round a third time (test the WALKER against a synthetic warn map, and leave
622650 // the ledger-driven coupling to the assertions that are genuinely about the
@@ -820,40 +848,49 @@ describe('lintLivenessProperties', () => {
820848 // BROKEN walk, so it would have been green from the day the bug shipped and
821849 // pinned nothing.
822850 describe ( 'translation bundles are locale-keyed (#11288)' , ( ) => {
823- /** `flows` is the one `authorWarn` row on the shipped `translation` ledger. */
824- const flowsGroup = { lead_conversion : { screens : { screen_1 : { title : '转化详情' } } } } ;
851+ /**
852+ * A `flows` group as an author writes it. `flows` is `live` on the shipped
853+ * ledger, so it is authored below only where the pin is SILENCE; the walk
854+ * pins author the planted {@link SYNTH_WARNED_GROUP} instead.
855+ */
856+ const flowsGroup = { lead_conversion : { label : '线索转化' , screens : { screen_1 : { title : '转化详情' } } } } ;
825857
826858 it ( 'warns on a warned group authored under a locale entry' , ( ) => {
827- const findings = lintLivenessProperties ( {
828- translations : [ { 'zh-CN' : { flows : flowsGroup } } ] ,
859+ const findings = lintWithPlantedGroup ( {
860+ translations : [ { 'zh-CN' : { [ SYNTH_WARNED_GROUP ] : synthWarnedGroup } } ] ,
829861 } ) ;
830- expect ( paths ( findings ) . some ( ( m ) => m . includes ( '`flows`' ) ) ) . toBe ( true ) ;
862+ expect ( paths ( findings ) . some ( ( m ) => m . includes ( `\` ${ SYNTH_WARNED_GROUP } \`` ) ) ) . toBe ( true ) ;
831863 expect ( findings . map ( ( f ) => f . where ) ) . toEqual ( [ "translation bundle #0 · locale 'zh-CN'" ] ) ;
832- expect ( findings [ 0 ] ?. hint ) . toContain ( 'screen-flow runner' ) ;
864+ expect ( findings [ 0 ] ?. hint ) . toBe ( SYNTH_WARNED_GROUP_HINT ) ;
833865 } ) ;
834866
835867 // The walk has two levels and both can stop early. Authored on the SECOND
836868 // locale of the SECOND bundle, so neither an outer nor an inner walk that
837869 // visits only index 0 can pass this.
838870 it ( 'reaches every locale of every bundle, not just the first of each' , ( ) => {
839- const findings = lintLivenessProperties ( {
871+ const findings = lintWithPlantedGroup ( {
840872 translations : [
841873 { en : { messages : { 'common.save' : 'Save' } } } ,
842874 {
843875 ja : { messages : { 'common.save' : '保存' } } ,
844- 'zh-CN' : { flows : flowsGroup } ,
876+ 'zh-CN' : { [ SYNTH_WARNED_GROUP ] : synthWarnedGroup } ,
845877 } ,
846878 ] ,
847879 } ) ;
848880 expect ( findings . map ( ( f ) => f . where ) ) . toEqual ( [ "translation bundle #1 · locale 'zh-CN'" ] ) ;
849881 } ) ;
850882
883+ // `flows` is among the live groups authored here: the shipped row went
884+ // `live` once the runner read the flow's own `label` as well as `screens`,
885+ // so a half-reverted flip (the row `live` again but still opted in to a
886+ // warning, or `planned` again) surfaces as a finding on this bundle.
851887 it ( 'stays silent on a bundle that authors only live groups' , ( ) => {
852888 const findings = lintLivenessProperties ( {
853889 translations : [ {
854890 'zh-CN' : {
855891 objects : { crm_lead : { label : '线索' } } ,
856892 messages : { 'common.save' : '保存' } ,
893+ flows : flowsGroup ,
857894 } ,
858895 } ] ,
859896 } ) ;
@@ -869,7 +906,7 @@ describe('lintLivenessProperties', () => {
869906 it ( 'the translation silence is a real verdict, not a lint that stopped loading ledgers' , ( ) => {
870907 const findings = lintLivenessProperties ( {
871908 objects : [ { name : 'widget' , externalSharingModel : 'read' } ] ,
872- translations : [ { 'zh-CN' : { objects : { crm_lead : { label : '线索' } } } } ] ,
909+ translations : [ { 'zh-CN' : { objects : { crm_lead : { label : '线索' } } , flows : flowsGroup } } ] ,
873910 } ) ;
874911 const messages = findings . map ( ( f ) => f . message ) ;
875912 expect ( messages . some ( ( m ) => m . includes ( 'externalSharingModel' ) ) ) . toBe ( true ) ;
@@ -882,23 +919,26 @@ describe('lintLivenessProperties', () => {
882919 // (`packages/spec/src/kernel/metadata-type-schemas.ts:158`), not this
883920 // collection. It warns on the broken walk and must not here:
884921 // `stack.translations` is `z.record(LocaleSchema, TranslationDataSchema)`, so
885- // this object would have to mean a locale named `flows` whose value is
886- // `TranslationData` — a parse error two tiers before this advisory ever runs.
922+ // this object would have to mean a locale named after the group, whose value
923+ // is `TranslationData` — a parse error two tiers before this advisory runs.
887924 // Runtime-authored items reach this lint through no door at all: no stack
888925 // collection carries them, and the rule is `surfaces: CLI_ONLY`, so it does
889926 // not run at the runtime publish gate either.
927+ // Driven against the planted ledger, where the same group authored under a
928+ // locale DOES warn (the first pin of this block): on the shipped ledger no
929+ // translation group warns, so a silence there would pin nothing.
890930 it ( 'does not treat a runtime `TranslationItem` shape as a bundle' , ( ) => {
891- const findings = lintLivenessProperties ( {
892- translations : [ { name : 'zh_cn' , locale : 'zh-CN' , flows : flowsGroup } ] ,
931+ const findings = lintWithPlantedGroup ( {
932+ translations : [ { name : 'zh_cn' , locale : 'zh-CN' , [ SYNTH_WARNED_GROUP ] : synthWarnedGroup } ] ,
893933 } ) ;
894934 expect ( findings ) . toEqual ( [ ] ) ;
895935 } ) ;
896936
897937 // "Advisory only — returns findings, never throws" is the function's own
898938 // contract, and a bundle walk adds two levels that can be malformed.
899939 it ( 'never throws on a malformed bundle, and keeps walking past it' , ( ) => {
900- const findings = lintLivenessProperties ( {
901- translations : [ null , { 'zh-CN' : null } , { en : { flows : flowsGroup } } ] ,
940+ const findings = lintWithPlantedGroup ( {
941+ translations : [ null , { 'zh-CN' : null } , { en : { [ SYNTH_WARNED_GROUP ] : synthWarnedGroup } } ] ,
902942 } ) ;
903943 // The bundle is the third list item the author wrote and is reported as
904944 // `#1`: since #15636 this walk reads through `recordsOf`, which drops the
@@ -1098,8 +1138,10 @@ describe('the array fan-out, against a synthetic warn map (#10262)', () => {
10981138// the `dead` branch, so the finding's MESSAGE told the author to remove
10991139// something the ledger's own `authorHint`/`note` on the SAME finding said to
11001140// keep. `field.relatedListFilter`, `object.externalSharingModel` and
1101- // `translation.flows` are the three shipped rows this hit — two of them still,
1102- // the third only until #19187 flipped `field.relatedListFilter` `live`.
1141+ // `translation.flows` are the three shipped rows this hit. Only
1142+ // `object.externalSharingModel` still does: `field.relatedListFilter` and
1143+ // `translation.flows` have both gone `live`, each once its reader reached the
1144+ // `.objectui-sha` pin.
11031145//
11041146// The real ledgers currently have PLANNED rows and EXPERIMENTAL rows, but — as
11051147// this file's other comments document at length (#2377, #3896, #4509) — no
@@ -1114,29 +1156,48 @@ describe('dead / experimental / planned / live-elsewhere verdicts are distinct,
11141156 const oneEntry = ( entry : Record < string , unknown > ) => new Map ( [ [ 'gizmo' , entry ] ] ) ;
11151157
11161158 // ── REAL LEDGER: the three rows the card captured ──────────────────────
1117- it ( 'REAL LEDGER: translation.flows (planned) — planned rule id, non-contradictory message, hint preserved' , ( ) => {
1159+ //
1160+ // `translation.flows` no longer has a planned REAL LEDGER case: it went
1161+ // `live` once the console's screen-flow runner read the flow's own `label`
1162+ // as well as `screens`, so it warns about nothing and can pin no verdict.
1163+ // What its case held is split in two below and nothing is dropped: the
1164+ // shipped row's SILENCE on the real ledger, and the planned verdict's wording
1165+ // WITH an authorHint (rule id, non-contradictory message, hint shown) on a
1166+ // planted translation group, the one shape no shipped row carries any more.
1167+ it ( 'REAL LEDGER: translation.flows is `live` — authoring it warns nothing, in a call that still warns' , ( ) => {
11181168 const findings = lintLivenessProperties ( {
1169+ objects : [ { name : 'widget' , externalSharingModel : 'read' } ] ,
11191170 translations : [ {
1120- 'zh-CN' : { flows : { lead_conversion : { screens : { screen_1 : { title : '转化详情' } } } } } ,
1171+ 'zh-CN' : { flows : { lead_conversion : { label : '线索转化' , screens : { screen_1 : { title : '转化详情' } } } } } ,
11211172 } ] ,
11221173 } ) ;
1174+ // Anti-vacuity: the same call, the same ledger load, still raises the one
1175+ // planned row in tree, so the silence is a reading and not an unloaded lint.
1176+ expect ( findings . map ( ( f ) => f . message ) . filter ( ( m ) => m . includes ( 'externalSharingModel' ) ) ) . toHaveLength ( 1 ) ;
1177+ expect ( findings . filter ( ( f ) => f . where . startsWith ( 'translation bundle' ) ) ) . toEqual ( [ ] ) ;
1178+ } ) ;
1179+
1180+ it ( 'PLANTED: a planned + authorWarn translation group with an authorHint — planned rule id, non-contradictory message, hint preserved' , ( ) => {
1181+ const findings = lintWithPlantedGroup ( {
1182+ translations : [ { 'zh-CN' : { [ SYNTH_WARNED_GROUP ] : synthWarnedGroup } } ] ,
1183+ } ) ;
11231184 expect ( findings ) . toHaveLength ( 1 ) ;
11241185 const [ f ] = findings ;
11251186 expect ( f . rule ) . toBe ( 'liveness-planned-property' ) ;
11261187 expect ( f . message ) . not . toContain ( 'dead' ) ;
11271188 expect ( f . message ) . toContain ( 'is planned' ) ;
1128- // The card 's own captured hint — unchanged by this fix, just no longer
1129- // contradicted by the message sitting next to it.
1130- expect ( f . hint ) . toContain ( 'screen-flow runner' ) ;
1189+ // The row 's own authorHint, shown as written and not contradicted by the
1190+ // message sitting next to it.
1191+ expect ( f . hint ) . toBe ( SYNTH_WARNED_GROUP_HINT ) ;
11311192 } ) ;
11321193
11331194 // ⚠️ The third of the card's rows, `field.relatedListFilter`, no longer has a
11341195 // REAL LEDGER case here: #19187 flipped it `live` once objectui#4664 reached
11351196 // the `.objectui-sha` pin, so it warns about nothing and can pin no verdict.
11361197 // Its silence is pinned in the `field related-list filter (#19187)` block
11371198 // above, where a half-reverted flip surfaces. The PLANNED branch stays a
1138- // contract test on the two rows that are still planned — `translation.flows`
1139- // just above and `object.externalSharingModel` just below.
1199+ // contract test on the one row that is still planned,
1200+ // `object.externalSharingModel` just below.
11401201
11411202 it ( 'REAL LEDGER: object.externalSharingModel (planned, no authorHint) — planned rule id, the planned DEFAULT hint, never its ledger note' , ( ) => {
11421203 const findings = lintLivenessProperties ( { objects : [ { name : 'widget' , externalSharingModel : 'read' } ] } ) ;
@@ -1343,30 +1404,54 @@ describe('dead / experimental / planned / live-elsewhere verdicts are distinct,
13431404// The walker now asks THIS function which groups are warned instead of reading
13441405// the same JSON a second time. These pins hold the two readings equal.
13451406describe ( 'authorWarnedProperties' , ( ) => {
1346- it ( 'reports the `flows` translation group as warned (the row the CLI gate reads)' , ( ) => {
1347- expect ( authorWarnedProperties ( 'translation' ) . has ( 'flows' ) ) . toBe ( true ) ;
1407+ it ( 'reports no translation group as warned now that `flows` is live (the set the CLI gate reads)' , ( ) => {
1408+ // `flows` was the one warned group, and it went `live` once the runner read
1409+ // both its halves. An equality, so the day a translation group acquires a
1410+ // warn this goes red and the CLI's coverage gate is looked at with it.
1411+ expect ( [ ...authorWarnedProperties ( 'translation' ) ] ) . toEqual ( [ ] ) ;
1412+ // The positive the empty answer needs, from the same reader in the same
1413+ // process: the one planned + authorWarn row still in tree.
1414+ expect ( authorWarnedProperties ( 'object' ) . has ( 'externalSharingModel' ) ) . toBe ( true ) ;
13481415 } ) ;
13491416
13501417 it ( 'can say no — a live group in the same ledger is absent' , ( ) => {
1351- // The negative half of the instrument: `objects` is `live` and sits in the
1352- // same file as the positive above, so a set that swallowed everything or
1353- // returned everything would fail one of the two.
1418+ // The negative half of the instrument, beside the positive above (a warned
1419+ // `object` row): a set that swallowed everything or returned everything
1420+ // would fail one of the two.
13541421 const warned = authorWarnedProperties ( 'translation' ) ;
13551422 expect ( warned . has ( 'objects' ) ) . toBe ( false ) ;
13561423 expect ( warned . has ( 'pages' ) ) . toBe ( false ) ;
1424+ expect ( warned . has ( 'flows' ) ) . toBe ( false ) ;
13571425 } ) ;
13581426
1359- it ( 'agrees with the rule itself on every path it names — the anti-drift pin' , ( ) => {
1427+ it ( 'agrees with the rule itself on every translation group — the anti-drift pin' , ( ) => {
13601428 // The whole point of exporting this is that the demand side and the warn
1361- // side cannot disagree. So: authoring each warned top-level group in a
1362- // locale bundle must actually produce a finding naming that group. A path
1363- // this set reports but the rule stays silent on is the drift that reopens
1364- // the collision from the other end.
1365- for ( const path of authorWarnedProperties ( 'translation' ) ) {
1366- if ( path . includes ( '.' ) ) continue ; // container rows only — see getNested
1367- const findings = lintLivenessProperties ( {
1368- translations : [ { 'zh-CN' : { [ path ] : { probe : { label : 'x' } } } } ] ,
1369- } ) ;
1429+ // side cannot disagree. With no translation group warned, a loop over the
1430+ // exported set would assert nothing, so this asks over the WHOLE domain:
1431+ // every top-level row the shipped `translation` ledger governs is authored
1432+ // in one locale entry, and the groups the rule names must be exactly the
1433+ // groups this set names, in both directions.
1434+ const ledger = JSON . parse ( readFileSync ( join ( shippedLedgerDir ( ) , 'translation.json' ) , 'utf8' ) ) ;
1435+ const groups = Object . keys ( ledger . props ) ;
1436+ expect ( groups ) . toContain ( 'flows' ) ;
1437+ expect ( groups . length ) . toBeGreaterThanOrEqual ( 10 ) ;
1438+ const findings = lintLivenessProperties ( {
1439+ translations : [ { 'zh-CN' : Object . fromEntries ( groups . map ( ( g ) => [ g , { probe : { label : 'x' } } ] ) ) } ] ,
1440+ } ) ;
1441+ const named = groups . filter ( ( g ) => findings . some ( ( f ) => f . message . includes ( `sets \`${ g } \`` ) ) ) ;
1442+ const exported = [ ...authorWarnedProperties ( 'translation' ) ] . filter ( ( p ) => ! p . includes ( '.' ) ) ;
1443+ expect ( named . sort ( ) ) . toEqual ( exported . sort ( ) ) ;
1444+ } ) ;
1445+
1446+ it ( 'agrees with the rule itself on every object path it names — the same pin over a non-empty set' , ( ) => {
1447+ // The direction the translation pin above cannot exercise today ("a path
1448+ // this set reports but the rule stays silent on"), held on the type that
1449+ // still has a warned row. `true` is authored because the rule warns on a
1450+ // boolean only when it is true and on anything else when present.
1451+ const warned = [ ...authorWarnedProperties ( 'object' ) ] . filter ( ( p ) => ! p . includes ( '.' ) ) ;
1452+ expect ( warned . length ) . toBeGreaterThan ( 0 ) ;
1453+ for ( const path of warned ) {
1454+ const findings = lintLivenessProperties ( { objects : [ { name : 'widget' , [ path ] : true } ] } ) ;
13701455 expect ( findings . map ( ( f ) => f . message ) . join ( '\n' ) ) . toContain ( `sets \`${ path } \`` ) ;
13711456 }
13721457 } ) ;
0 commit comments