Repository navigation
Expand file tree
/
Copy pathindex.html
More file actions
5565 lines (5540 loc) · 387 KB
/
Copy pathindex.html
File metadata and controls
5565 lines (5540 loc) · 387 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline' https://accounts.google.com/gsi/client; img-src 'self' data:; manifest-src 'self'; style-src 'unsafe-inline' https://accounts.google.com/gsi/style; connect-src https://www.googleapis.com https://accounts.google.com/gsi/ https://script.google.com https://script.googleusercontent.com; frame-src https://accounts.google.com/gsi/; base-uri 'none'; object-src 'none'">
<title>Sentence Workshop</title><style>
:root{color-scheme:light;--ink:#203343;--muted:#617080;--blue:#245d89;--line:#dce4e8;--green:#26705b;--correction:#a51d2c}*{box-sizing:border-box}body{margin:0;color:var(--ink);background:#f3f6f5;font:17px/1.5 system-ui,Segoe UI,sans-serif}main{max-width:900px;margin:auto;padding:32px 24px 50px}header{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:30px}h1{font-size:27px;letter-spacing:-.04em;margin:0}h2{font-size:25px;line-height:1.4;margin:7px 0 6px}h3{font-size:18px;margin:16px 0 8px}p{margin:8px 0}.hint{color:var(--muted);font-size:14px}.eyebrow{font-size:12px;font-weight:750;text-transform:uppercase;letter-spacing:.1em;color:var(--green)}.card{background:white;border:1px solid var(--line);border-radius:18px;padding:28px}.instruction{margin-bottom:25px}button{font:inherit;font-weight:600;min-height:44px;padding:9px 15px;border:1px solid var(--line);border-radius:9px;background:white;color:var(--ink);cursor:pointer}button:hover:not(:disabled){border-color:var(--blue);background:#eef4fa}button.primary{background:var(--blue);border-color:var(--blue);color:white}button.primary:hover:not(:disabled){background:#184a72}button:disabled{opacity:.45;cursor:default}button:focus-visible,summary:focus-visible{outline:3px solid var(--blue);outline-offset:3px}.quiet{border-color:transparent;background:transparent;font-size:14px;color:var(--muted)}.workspace{display:flex;align-items:baseline;position:relative;align-content:flex-start;flex-wrap:wrap;gap:9px;min-height:80px;border:2px dashed #bccedb;border-radius:12px;padding:10px 12px;background:#fcfdff}.workspace button,.word-list button{font-size:21px;min-height:40px;height:40px;padding:2px 8px;line-height:30px;cursor:grab;user-select:none}.workspace button:active,.word-list button:active{cursor:grabbing}.workspace button[aria-pressed=true]{border-color:var(--blue);background:#e6effa;outline:2px solid var(--blue)}.workspace button.active-correction{outline:2px solid var(--correction);outline-offset:2px}.correction-steps{list-style:none;margin:8px 0;padding:0}.correction-step{display:flex;align-items:baseline;gap:8px;width:100%;text-align:left;min-height:44px;padding:5px 6px;margin:3px 0;background:transparent;border-color:transparent;font-size:14px;font-weight:500}.step-number{flex:0 0 28px;line-height:28px;text-align:center;color:var(--correction);font-weight:700;border-radius:50%}.correction-step[aria-current=step] .step-number{background:#e1e4e8}.correction-step:hover:not(:disabled){border-color:transparent;background:#eceef1}.correction-step:disabled{opacity:.6;cursor:default;color:var(--muted)}.workspace button.drop-before::before,.workspace button.drop-after::before,.workspace.drop-empty::before{content:"";position:absolute;top:-3px;width:10px;height:calc(100% + 6px);background:var(--blue);clip-path:polygon(0 0,100% 0,60% 10%,60% 90%,100% 100%,0 100%,40% 90%,40% 10%);pointer-events:none;z-index:1}.workspace button.drop-before::before{left:-10px}.workspace button.drop-after::before{right:-10px}.workspace.drop-empty::before{left:2px;top:10px;height:46px}.placeholder{color:#758593;align-self:center;font-size:16px;pointer-events:none}.word-options{position:fixed;z-index:20;display:flex;gap:6px;flex-wrap:wrap;width:max-content;max-width:min(360px,calc(100vw - 16px));max-height:calc(100vh - 16px);overflow-y:auto;padding:8px;margin:0;background:white;border:1px solid #c5d2dd;border-radius:12px;box-shadow:0 8px 24px #20334326}.word-options button{display:inline-flex;flex-direction:column;align-items:center;justify-content:center;padding:7px 11px;min-height:44px;line-height:1.25;font-size:16px}.word-options button span,.word-options button small{display:block}.word-options button small{font-size:11px;color:var(--muted);font-weight:500}.word-options button[aria-pressed=true]{border-color:var(--blue);background:#dce9f7}.word-options .capital-toggle{background:#edf0f3}.word-options .capital-toggle[aria-pressed=true],.word-options .capital-toggle:hover:not(:disabled){background:#d7dde3}.word-box{margin:0;border:1px solid var(--line);border-radius:12px;padding:10px 14px 14px;background:#f5f8f9}.word-list{display:flex;gap:16px 24px;flex-wrap:wrap;align-items:flex-start}.bank-group{min-width:0}.bank-group h4{margin:0 0 7px;font-size:11px;font-weight:600;color:var(--muted)}.group-words{display:flex;flex-wrap:wrap;gap:8px}.word-list button{background:white}.punctuation{display:flex;gap:8px}.punctuation button{font-size:20px;height:40px;padding:2px 17px;line-height:30px}.actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center}.actions .spacer{flex:1}.feedback{margin-top:16px;border-radius:10px;padding:13px;background:#f3f5f7}.feedback.pass{background:#eaf5ee;color:#21654d}.feedback p{font-size:16px}#edit-status:empty{display:none}details{margin:14px 0}summary{cursor:pointer;font-weight:600;font-size:15px}[hidden]{display:none!important}@media(max-width:600px){main{padding:18px 12px 32px}header{margin-bottom:20px}h1{font-size:23px}h2{font-size:22px}.card{padding:18px}.workspace{padding:10px 12px}.workspace button,.word-list button{font-size:19px;padding:2px 8px}.word-box{padding:10px 12px 12px}}
/* A compact native lesson menu and the same labelled colour cues on both tile surfaces. */
main{max-width:1440px}.app-layout{display:grid;grid-template-columns:260px minmax(0,1fr);gap:20px;align-items:start}.lesson-nav{min-width:0;padding:16px;background:white;border:1px solid var(--line);border-radius:14px}.lesson-menu{margin:0}.lesson-menu>summary{font-size:18px}.chapter{margin:15px 0 0}.chapter summary{font-size:15px;padding:4px 0}.chapter-label{display:block;font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.08em}.chapter strong{font-weight:650}.chapter button{display:block;width:100%;margin-top:7px;padding:9px 10px;text-align:left;font-size:14px;font-weight:500}.chapter button[aria-current=page]{background:#e8f0fa;border-color:#8aaace;color:#184a72;font-weight:700}#challenge-title:focus{outline:none}[data-pos=noun]{--pos-bg:#e8f1ff}[data-pos=verb]{--pos-bg:#d9efdf}[data-pos=pronoun]{--pos-bg:#f0ebfa}[data-pos=determiner]{--pos-bg:#fff4d4}[data-pos=adjective]{--pos-bg:#fbeaf1}[data-pos=preposition]{--pos-bg:#f5c4aa}[data-pos=adverb]{--pos-bg:#fff0df}[data-pos=punctuation]{--pos-bg:#edf0f3}[data-pos="pronoun determiner"]{--pos-bg:linear-gradient(110deg,#f0ebfa 50%,#fff4d4 50%)}[data-pos="noun adverb"]{--pos-bg:linear-gradient(110deg,#e8f1ff 50%,#fff0df 50%)}.word-list button.pos-tile,.workspace button.pos-tile,.punctuation button.pos-tile{background:var(--pos-bg,#f0f2f4);color:var(--ink)}.pos-tile::after{content:attr(data-pos-label);display:block;font-size:10px;line-height:1.5;font-weight:700;letter-spacing:.02em;cursor:inherit}.punctuation button.pos-tile{min-height:40px}@media(max-width:1000px){.app-layout{grid-template-columns:1fr;gap:16px}.lesson-nav{padding:12px 16px}.chapter button{max-width:none}}
body{min-height:100vh}.choice-trays{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:start;gap:16px;margin:12px 0 24px}.choice-tray{min-width:0}.choice-tray>h3{margin:0 0 6px;font-size:16px;color:var(--muted)}.punctuation-box{padding:12px;border:1px solid var(--line);border-radius:12px;background:#f5f8f9}.punctuation-box .pos-tile::after{display:none}@media(max-width:600px){.choice-trays{grid-template-columns:1fr}.choice-tray:last-child{justify-self:start}}.word-list .pos-tile::after{display:none}.word-box.return-target,.punctuation-box.return-target{outline:3px solid var(--blue);background:#eaf1fa}
.instruction{margin-bottom:12px}.activity-heading{display:flex;align-items:center;justify-content:space-between;gap:8px 20px;flex-wrap:wrap}.activity-heading h2{flex:1;margin:4px 0}.task-requirements{margin:8px 0 0;font-size:14px}.task-requirements span{white-space:pre-wrap}.framework{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;align-items:start;margin:12px 0 16px}.keyboard-help{white-space:nowrap;font-size:12px;color:var(--muted)}.idea-card{min-width:0;padding:12px 14px;background:#f5f8f9;border-radius:9px}.idea-card h3{font-size:16px;margin:0 0 10px}.subject-number{margin-left:8px;font-size:13px;font-weight:400;color:var(--muted)}.idea-facts{display:grid;grid-template-columns:max-content minmax(0,1fr);gap:5px 12px;margin:0;font-size:14px}.idea-fact{display:contents}.idea-fact dt{font-weight:700}.fact-words{margin:0;overflow-wrap:anywhere}.time-position{font-size:12px;color:var(--muted)}.meaning-cue{font-size:13px;color:var(--muted);margin:8px 0}.lesson-card{margin:0;font-size:14px}.lesson-card>summary{display:flex;align-items:center;gap:7px;min-height:44px;color:var(--green);font-size:13px}.lesson-card>summary::before{content:'▸'}.lesson-card[open]>summary::before{content:'▾'}.lesson-card[open]{flex-basis:100%;order:2;padding:10px 14px;background:#f5f8f9;border-radius:9px}.lesson-card h3{margin:10px 0 5px;font-size:16px}.lesson-label{font-size:12px;font-weight:700;color:var(--muted)}.lesson-pattern{font-size:14px}.lesson-example{background:white;padding:10px 12px;border-radius:8px}.builder-heading{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:8px}.builder-heading h3{font-size:16px;margin:0}#building-help{font-size:13px;margin:2px 0 8px}.workspace{margin-top:8px;border-style:solid;border-width:1px;min-height:80px}.workspace.insert-at-start{border-left:4px solid var(--blue)}.word-list{flex-direction:column;gap:8px}.bank-group{display:grid;grid-template-columns:100px minmax(0,1fr);gap:8px;width:100%;align-items:start}.bank-group h4{font-size:13px;font-weight:700;color:var(--ink);margin:5px 0 0}.card{padding:20px}.choice-tray h3{font-size:16px;margin:12px 0 8px}@media(max-width:600px){.card{padding:16px}.bank-group{grid-template-columns:90px minmax(0,1fr)}.activity-heading{gap:0 12px}}
@media(max-width:600px){.framework{grid-template-columns:1fr}}
.score-item{display:inline-block;font-size:12px;margin:7px 10px 0 0;color:var(--ink)}
[data-pos=conjunction]{--pos-bg:#d6eef4}[data-pos="adjective adverb"]{--pos-bg:linear-gradient(110deg,#fbeaf1 50%,#fff0df 50%)}#next-chapter{margin-top:16px}
.workspace button.pos-tile{position:relative;margin-bottom:18px}.workspace button.pos-tile::after{position:absolute;top:calc(100% + 4px);left:50%;width:max-content;transform:translateX(-50%);white-space:nowrap;text-align:center;color:var(--muted);font-weight:500;line-height:14px;pointer-events:none}.punctuation button{flex:0 0 44px;width:44px;min-width:44px;padding-left:4px;padding-right:4px}.workspace button.punctuation-tile{flex:0 0 32px;width:32px;min-width:32px;min-height:40px;height:40px;align-self:baseline;padding:2px;line-height:20px;font-size:20px}.workspace button.punctuation-tile::after{font-size:12px;line-height:16px}
.mixed-review{width:100%;margin-top:18px;text-align:left;font-size:14px}.mixed-review[aria-current=page]{background:#e8f0fa;border-color:#8aaace}
.review-answers{margin-top:26px}.review-answers ol{padding-left:26px}.review-answers>ol>li{padding:16px;margin:14px 0;border:1px solid var(--line);border-radius:12px}.review-answers h4{margin:0}.answer-comparison{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:12px}.answer-comparison>div{min-width:0;background:#f5f8f9;border-radius:8px;padding:12px}.answer-comparison strong{font-size:13px;color:var(--muted)}.answer-comparison p{overflow-wrap:anywhere}.answer-feedback{font-size:14px;color:var(--ink)}@media(max-width:800px){.answer-comparison{grid-template-columns:1fr}}
.actions{position:sticky;bottom:0;z-index:5;padding:8px 0;background:white;border-top:1px solid var(--line)}.skip-notice{display:flex;align-items:center;gap:12px;margin:12px 0;padding:8px 12px;border-radius:8px;background:#f3f5f7;font-size:14px}.activity-shortcut{position:absolute;left:12px;top:8px;transform:translateY(-200%);z-index:30;padding:8px 12px;background:white;color:var(--blue);border-radius:8px}.activity-shortcut:focus{transform:none;outline:3px solid var(--blue)}
.account-menu{margin:-12px 0 20px;font-size:14px}.account-menu>summary{font-size:13px;color:var(--muted)}.account-menu label{display:flex;flex-direction:column;gap:4px;font-size:12px;color:var(--muted)}input{font:inherit;color:var(--ink);border:1px solid var(--line);border-radius:7px;min-height:40px;padding:6px 8px;background:white;max-width:100%}.backup-label{cursor:pointer}.backup-label input{max-width:240px;font-size:12px}.course-progress{margin:16px 0 0;padding:12px 0 0;border-top:1px solid var(--line);font-size:12px;color:var(--muted)}.course-progress>summary{font-size:13px;color:var(--green)}.progress-details{display:flex;flex-direction:column;gap:5px;padding-top:10px}.course-progress small{font-size:12px}.chapter-progress{display:block;font-size:11px;font-weight:400;color:var(--muted);margin-top:4px}.chapter button{position:relative;padding-right:10px}.lesson-status{display:block;margin-top:4px;font-size:12px;line-height:1.35;font-weight:400;color:var(--muted);overflow-wrap:anywhere}.chapter button[data-status="Ready for review"] .lesson-status,.chapter button[data-status="Checkpoint passed"] .lesson-status{color:var(--green)}.chapter button[aria-current=page]{border-left:4px solid var(--blue)}.mixed-review{border-top:2px solid var(--line)}.workspace button.pos-tile::after{content:attr(data-pos-name);font-size:12px;line-height:16px;letter-spacing:0}.workspace button.pos-tile,.word-list button.pos-tile,.punctuation button.pos-tile{box-shadow:0 2px 0 #20334312}.actions #undo,.actions #clear{border-color:var(--line)}.checkpoint-feedback{margin:16px 0;padding:14px;background:#f3f5f7;border-radius:10px;font-size:14px}.checkpoint-feedback button{margin:4px;font-size:13px}#studied-review{margin:0 0 14px;font-size:13px}
.google-controls{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap}.google-controls button,.google-controls a{font-size:13px}.google-controls a{display:inline-flex;align-items:center;min-height:44px;padding:9px 15px;border:1px solid var(--line);border-radius:9px;background:white;color:var(--ink);text-decoration:none;font-weight:600}.google-controls a:hover{border-color:var(--blue);background:#eef4fa}.google-controls a:focus-visible{outline:3px solid var(--blue);outline-offset:3px}.google-controls span{font-size:12px;color:var(--muted);overflow-wrap:anywhere}#google-status{font-size:12px;margin:-18px 0 20px;color:var(--muted)}
#google-dot{width:8px;height:8px;border-radius:50%;background:#94a3b8;display:inline-block;flex:0 0 auto}#google-dot[data-state="synced"]{background:#10b981}#google-dot[data-state="syncing"]{background:#f59e0b}#google-dot[data-state="error"]{background:#ef4444}
header{flex-wrap:wrap;gap:14px 24px}.brand{display:flex;align-items:center;gap:12px;min-width:0}.brand-mark{display:block;flex:0 0 48px;width:48px;height:48px}.brand h1{font-family:"Trebuchet MS","Segoe UI",sans-serif;font-size:clamp(24px,2.5vw,34px);font-weight:800;letter-spacing:-.045em;line-height:1.15}.brand h1 span{color:var(--green)}.google-controls{min-width:0;margin-left:auto}@media(max-width:600px){.brand{gap:10px}.brand-mark{flex-basis:40px;width:40px;height:40px}.brand h1{font-size:24px}}
.course-start,.recovery-notice{padding:14px 16px;border:1px solid var(--line);border-radius:12px;background:#edf4f2;margin:0 0 16px}.course-start p{font-size:13px;color:var(--muted);margin:4px 0 0}.course-overview{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;margin:0 0 16px;font-size:14px;color:var(--green)}.recovery-notice p{font-size:13px}.recovery-notice button{font-size:13px}.chapter button:disabled{opacity:1;color:var(--muted);background:#f7f9fa}.chapter button[data-status="Chapter passed"] .lesson-status{color:var(--green)}#lesson-next{margin:0 0 10px;color:var(--green)}
/* Lesson art replaces duplicate introductions and scales without cropping. */
.activity-heading{display:block}.lesson-card,.lesson-card[open]{padding:0;background:transparent;border-radius:0}.lesson-card>summary{width:max-content;max-width:100%;font-size:14px}.lesson-infographic{display:block;width:100%;height:auto;margin:8px 0 16px;border-radius:10px}.visually-hidden{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip-path:inset(50%);white-space:nowrap!important;border:0!important}.review-lock{margin:16px 0 0;font-size:14px;color:var(--muted)}
/* Answer colors and labels are added only after practice checks or a finished review. */
#intro-practice .actions{position:static}
.intro-sentence{display:flex;align-items:baseline;flex-wrap:wrap;gap:14px 30px;padding:20px 14px 12px;margin:12px 0 18px;border:1px solid var(--line);border-radius:12px;background:#fcfdff}
.intro-word{display:inline-block;position:relative;font-size:21px;font-weight:600;line-height:30px;min-height:44px;padding:5px 8px;border:1px solid var(--line);border-radius:9px;background:#edf0f3;color:var(--ink)}
button.intro-word{cursor:pointer}button.intro-word:disabled{opacity:1}.intro-word[aria-pressed=true]{outline:2px solid var(--blue);outline-offset:2px;background:#e6effa}.intro-word.intro-target{border:2px solid var(--blue);padding:4px 7px}.intro-word.intro-revealed{background:var(--pos-bg,#edf0f3);margin-bottom:20px;min-width:90px;text-align:center}.intro-revealed::after{content:attr(data-pos-name);position:absolute;top:calc(100% + 4px);left:50%;transform:translateX(-50%);white-space:nowrap;text-align:center;font-size:12px;font-weight:500;line-height:16px;color:var(--muted)}
.intro-choices{display:flex;flex-wrap:wrap;gap:8px;margin:12px 0 20px}.intro-choices button[aria-pressed=true]{outline:2px solid var(--blue);outline-offset:2px;background:#e6effa}#intro-feedback table{width:100%;max-width:450px;border-collapse:collapse;margin:12px 0}#intro-feedback th,#intro-feedback td{text-align:left;padding:6px 10px;border-bottom:1px solid var(--line)}#intro-feedback ol{padding-left:24px}#intro-feedback li{margin:20px 0}#intro-feedback>button{margin:5px}#intro-title:focus{outline:none}@media(max-width:600px){.intro-sentence{gap:12px 22px;padding:14px 10px}.intro-word{font-size:19px}.intro-word.intro-revealed{min-width:82px}.intro-revealed::after{font-size:12px;line-height:16px}}
.student-work-area,#intro-practice{margin:18px 0 16px;padding:16px 18px;background:#edf5fb;border:2px solid #789ebc;border-radius:14px;box-shadow:0 3px 10px #245d8910}
.student-work-area:focus-within,#intro-practice:focus-within{border-color:var(--blue);box-shadow:0 0 0 3px #245d8917}
.student-work-area .builder-heading{margin-top:0}.student-work-area .builder-heading h3{font-size:18px;color:var(--blue)}.student-work-area #building-help{margin:5px 0 10px;color:#40566b}
.student-work-area .workspace{background:white;border:1px solid #90abc0;min-height:96px;margin-top:0;padding:12px 14px}.student-work-area .workspace.insert-at-start{border-left:4px solid var(--blue)}
#intro-practice #intro-prompt{margin-top:0;color:var(--blue)}#intro-practice #intro-help{color:#40566b}#intro-practice .intro-sentence{background:white;border-color:#90abc0}#intro-practice .actions{background:transparent;border-top-color:#bdcfdd}
@media(max-width:600px){.student-work-area,#intro-practice{padding:13px 12px}.student-work-area .workspace{padding:12px}}
.intro-subtitle{margin:5px 0 12px;color:var(--muted);font-size:14px}.chapter-subtitle{display:block;margin-top:4px;color:var(--muted);font-size:12px;line-height:1.4}
button.advance{background:#246b4b;border-color:#246b4b;color:#fff;font-weight:650}button.advance:hover:not(:disabled){background:#195238;border-color:#195238}button.advance:focus-visible{outline:3px solid #173f2e;outline-offset:3px}button.advance:disabled{background:#eef2f0;border-color:#d5dfd9;color:#596d60}
.course-complete{margin:16px 0;padding:24px;background:#eaf5ee;border:1px solid #bad8c5;border-radius:14px;color:#204c38}.completion-mark{display:flex;align-items:center;justify-content:center;width:64px;height:64px;background:white;border-radius:50%;font-size:40px;font-weight:700;color:#246b4b}.completion-head{font-size:22px;font-weight:700;margin:16px 0 8px}.course-complete p{max-width:56ch}.completion-actions{display:flex;gap:12px;flex-wrap:wrap;margin-top:20px}.completion-actions button{min-height:48px}.completion-results{margin-top:20px}#completion-revisit{background:white;color:#246b4b}#completion-revisit:hover:not(:disabled){background:#dceee2}@media(max-width:600px){.course-complete{padding:18px}.completion-actions{flex-direction:column;align-items:stretch}}
.workspace button.pos-tile{margin-bottom:22px}.intro-word.intro-revealed{margin-bottom:24px}
.touch-help,.touch-art{display:none}.touch-edit{display:flex;flex-wrap:wrap;gap:6px;flex-basis:100%;padding-top:8px;border-top:1px solid var(--line)}.touch-edit button{flex:1 1 calc(50% - 6px)}
.word-options{max-height:calc(100dvh - 16px)}.actions{padding-bottom:calc(8px + env(safe-area-inset-bottom,0px))}
#art-dialog{width:calc(100% - 24px);max-width:1200px;max-height:calc(100dvh - 24px);padding:12px;border:1px solid var(--line);border-radius:12px;color:var(--ink)}#art-dialog::backdrop{background:#20334399}.art-toolbar{display:flex;justify-content:space-between;align-items:center;gap:12px}.art-toolbar h2{font-size:18px;margin:0}.art-scroll{overflow:auto;max-height:calc(100dvh - 160px);margin-top:10px}.art-scroll img{display:block;width:max(900px,100%);height:auto}.art-scroll p{font-size:16px;max-width:70ch;margin:12px}
@media(max-width:1000px){.lesson-menu>summary{min-height:44px;display:flex;align-items:center;font-size:16px}.lesson-menu>summary::before{content:'▸';margin-right:8px}.lesson-menu[open]>summary::before{content:'▾'}.lesson-nav{padding:8px 12px}.touch-art{display:inline-flex;margin:0 0 8px}}
@media(any-pointer:coarse){.desktop-help,.keyboard-help{display:none}.touch-help{display:inline}.touch-art{display:inline-flex;margin:0 0 8px}.workspace button,.word-list button,.punctuation button,.workspace button.punctuation-tile{min-width:44px;min-height:44px;height:44px;cursor:pointer}.workspace button.punctuation-tile{flex-basis:44px;width:44px}#intro-practice .actions{position:sticky;bottom:0;background:#edf5fb}}
@media(max-width:600px){.student-work-area,#intro-practice{padding:12px 10px}.student-work-area .workspace{padding:10px}.bank-group{display:block}.bank-group h4{margin:0 0 5px}.word-list{gap:12px}.choice-trays{grid-template-columns:minmax(0,1fr) auto;gap:10px}.punctuation-box{padding:8px}.punctuation{flex-direction:column;gap:8px}.word-box{padding:10px}.choice-tray h3{font-size:14px}.student-work-area #building-help{font-size:14px}.actions .spacer{display:none}.actions #skip-pair{margin-left:auto}}
.lesson-card,.art-scroll{position:relative}.lesson-image-status{position:absolute;left:0;right:0;margin:8px 0;z-index:1}
</style><meta name="theme-color" content="#245d89">
<meta name="apple-mobile-web-app-title" content="Sentence Workshop">
<link rel="icon" href="favicon.ico" sizes="16x16 24x24 32x32 48x48 64x64 128x128 256x256">
<link rel="icon" type="image/png" sizes="32x32" href="icons/favicon-32.png">
<link rel="icon" type="image/png" sizes="16x16" href="icons/favicon-16.png">
<link rel="icon" type="image/svg+xml" sizes="any" href="favicon.svg">
<link rel="apple-touch-icon" sizes="152x152" href="icons/apple-touch-icon-152.png">
<link rel="apple-touch-icon" sizes="167x167" href="icons/apple-touch-icon-167.png">
<link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png">
<link rel="manifest" href="site.webmanifest">
</head><body id="app-body"><a id="activity-shortcut" class="activity-shortcut" href="#challenge-title">Skip to activity</a><main>
<header><div class="brand"><svg class="brand-mark" viewBox="0 0 48 48" aria-hidden="true" focusable="false"><rect width="48" height="48" rx="14" fill="#245d89"/><rect x="9" y="11" width="18" height="10" rx="3" fill="#fff4d4"/><rect x="30" y="11" width="9" height="10" rx="3" fill="#d9efdf"/><rect x="9" y="26" width="10" height="10" rx="3" fill="#e8f1ff"/><rect x="22" y="26" width="13" height="10" rx="3" fill="#d9efdf"/><circle cx="39" cy="34" r="2" fill="#fff4d4"/></svg><h1>Sentence <span>Workshop</span></h1></div><div id="google-controls" class="google-controls"><span id="google-dot" data-state="signed-out" aria-hidden="true" title="Not signed in"></span><span id="google-account" hidden></span><button id="google-signin" disabled>Sign in with Google</button><a id="google-hosted" href="https://thepick.github.io/SentenceWorkshop/" target="_blank" rel="noopener" aria-label="Sign in with Google on the hosted website (opens a new tab)" hidden>Sign in with Google</a><button id="google-sync" class="quiet" hidden>Sync now</button><button id="google-signout" class="quiet" hidden>Sign out</button></div></header><p id="google-status" role="status"></p><p id="classroom-status" class="hint" role="status" hidden></p>
<details id="account-menu" class="account-menu" hidden><summary>Progress backup</summary><button id="export-progress" class="quiet">Save backup</button><label id="backup-import-label" class="backup-label">Import your backup<input id="import-progress" type="file" accept=".json,application/json"></label><p id="save-status" class="hint" role="status"></p></details>
<div id="course-start" class="course-start"><strong>Sign in to start your saved course.</strong><p>You can preview the introduction. Sign in with Google above to practice and keep your progress.</p></div><div id="course-overview" class="course-overview" hidden><strong id="course-total">0% complete</strong><button id="resume-review" class="quiet" hidden>Resume unfinished review</button></div>
<button id="studied-review" class="quiet" hidden>Review studied tenses</button>
<div id="app-layout" class="app-layout"><nav class="lesson-nav" aria-label="Chapters and lessons"><details id="lesson-menu" open class="lesson-menu"><summary id="lesson-menu-title">Lessons</summary><p id="review-lock" class="review-lock" role="status" hidden>Review in progress. Lessons reopen when you finish. Your answers are saved as you go.</p><div id="lessons"></div></details></nav>
<section id="intro-card" class="card" hidden>
<div class="eyebrow" id="intro-number">Words and their jobs · Lesson 1</div>
<p id="intro-subtitle" class="intro-subtitle">An introduction to parts of speech</p>
<h2 id="intro-title" tabindex="-1">Nouns</h2>
<details id="intro-lesson-card" class="lesson-card" open>
<summary>Lesson & example</summary>
<p id="intro-infographic-status" class="hint lesson-image-status" role="status" hidden></p><img id="intro-infographic" class="lesson-infographic" loading="lazy" alt=""><button id="intro-art-open" class="touch-art quiet" type="button">View larger</button>
<p id="intro-rule" class="visually-hidden"></p><p id="intro-tip" class="hint"></p>
</details>
<div id="intro-learning-actions"><p class="hint">Study the example. Then find this lesson’s words without colors or labels.</p><button id="intro-start" class="advance" disabled>Start practice</button></div>
<div id="intro-practice" hidden>
<h3 id="intro-prompt"></h3><p id="intro-help" class="hint"></p>
<div id="intro-sentence" class="intro-sentence" role="group" aria-label="Sentence words"></div>
<div id="intro-choices" class="intro-choices" role="group" aria-label="Answer choices" hidden></div>
<div class="actions"><button id="intro-check" class="primary" disabled>Check</button><button id="intro-continue" class="advance" disabled>Next question</button><button id="intro-see-lesson" class="quiet">Lesson & example</button><button id="intro-next-lesson" class="advance" hidden>Next lesson</button></div>
</div>
<div id="intro-feedback" class="feedback" role="status" tabindex="-1" hidden></div>
<details id="intro-progress" class="course-progress"><summary>Lesson progress</summary><p id="intro-progress-text" class="hint"></p></details>
</section>
<section id="sentence-card" class="card"><div class="instruction"><div class="activity-heading"><details id="lesson-card" class="lesson-card"><summary>Lesson & example</summary><p id="lesson-infographic-status" class="hint lesson-image-status" role="status" hidden></p><img id="lesson-infographic" class="lesson-infographic" loading="lazy" alt="" hidden><button id="lesson-art-open" class="touch-art quiet" type="button" hidden>View larger</button><div id="lesson-text"><h3 id="lesson-title"></h3><p id="tense-rule"></p><p id="lesson-position"></p><p class="lesson-label">Pattern</p><p id="lesson-pattern" class="lesson-pattern"></p><p class="lesson-label">Example</p><p id="lesson-example" class="lesson-example"></p></div></details><div class="eyebrow" id="pair-number">1.1 · Build 1</div><h2 id="challenge-title" tabindex="-1">Make a short sentence</h2></div><p id="task-requirements" class="task-requirements" aria-label="Sentence requirements"></p></div>
<div id="challenge-support"><p id="meaning-cue" class="meaning-cue" hidden></p><div id="framework-area" class="framework" aria-label="Idea requirements"></div></div>
<div id="recovery-notice" class="recovery-notice" hidden></div>
<section id="course-complete" class="course-complete" aria-labelledby="challenge-title" hidden>
<div class="completion-mark" aria-hidden="true">✓</div>
<p class="completion-head">You finished Sentence Workshop.</p>
<p>You completed Words and their jobs and all six chapters. Your progress is <strong>100% complete</strong>!</p>
<p>Keep your skills growing with a mixed-tense review, or choose a lesson to revisit. Your achievement stays earned.</p>
<div class="completion-actions"><button id="completion-practice" class="advance">Keep practicing</button><button id="completion-revisit" class="advance">Revisit lessons</button></div>
</section><div id="student-result" aria-live="polite" aria-atomic="true"></div>
<div id="skip-notice" class="skip-notice" role="status" hidden><span>Sentence skipped.</span><button id="undo-skip" class="quiet">Undo skip</button></div><div id="practice-area"><section class="student-work-area" aria-labelledby="sentence-work-title"><div class="builder-heading"><h3 id="sentence-work-title">Your sentence</h3></div><p id="building-help" class="hint"><span class="desktop-help">Add words from below. Click a word here to change it, or drag to move it.</span><span class="touch-help">Tap a word to change, move or remove it. Select a tile before adding words to place them after it.</span> <span class="keyboard-help">Home: insert at start.</span></p><div id="workspace" class="workspace" role="group" tabindex="0" aria-label="Your sentence" aria-keyshortcuts="Home" aria-description="Press Home, then choose a word to insert at the beginning."></div><div id="word-options" class="word-options" role="group" aria-label="Word options" hidden></div><p id="edit-status" class="hint" role="status"></p></section>
<div class="choice-trays"><section class="choice-tray" aria-labelledby="words-heading"><h3 id="words-heading">Words</h3><div id="word-box" class="word-box"><p id="return-hint" class="hint" hidden>Drop here to return a tile.</p><div id="word-bank" class="word-list" role="group" aria-label="Words to use"></div></div>
</section><section class="choice-tray" aria-labelledby="punctuation-heading"><h3 id="punctuation-heading">Punctuation</h3><div id="punctuation-box" class="punctuation-box"><div class="punctuation"><button id="comma" aria-label="Add a comma">,</button><button id="period" aria-label="Add a period">.</button></div></div></section></div>
<div class="actions"><button id="student-check" class="primary" disabled>Check</button><button id="continue" class="advance" hidden>Next sentence</button><button id="next-lesson" class="advance" hidden>Next lesson</button><button id="undo" class="quiet" disabled>Undo</button><button id="clear" class="quiet" disabled>Clear</button><span class="spacer"></span><button id="skip-pair" class="quiet">Skip</button></div></div><button id="next-chapter" class="advance" hidden>Next chapter</button><details id="course-progress" class="course-progress" aria-label="Practice readiness"><summary id="progress-summary">Progress</summary><div id="progress-details" class="progress-details"></div></details></section></div>
<p class="hint"><a href="privacy.html">Privacy and saved progress</a></p></main><dialog id="art-dialog" aria-labelledby="art-title"><div class="art-toolbar"><h2 id="art-title">Lesson example</h2><form method="dialog"><button>Close</button></form></div><div class="art-scroll"><p id="art-image-status" class="hint lesson-image-status" role="status" hidden></p><img id="art-image" alt=""><p id="art-description"></p></div></dialog><script data-cfasync="false">/* DATA — the reviewed vocabulary and finite phrase grammar. */
const VERSION = '0.7.6';
// Temporary beta access. Set false to restore Google sign-in and all sequence locks.
const TESTING_MODE = false;
// Keep the lesson art inside the downloadable app; no separate image files are required.
const LESSON_INFOGRAPHICS = {"simple_present":{"simple":{"src":"lesson-images/simple_present-simple-b07e4296bd4a.webp","width":1555,"height":655,"alt":"Simple sentences. Subject plus verb: who and action. The student walks. One person: walks. The students walk. More than one: walk. With I, you, we or they, use walk. Start with a capital letter and end with a period."},"details":{"src":"lesson-images/simple_present-details-eaee491a6752.webp","width":1555,"height":655,"alt":"Add an object or place. Subject plus verb plus detail. The student writes a letter. A letter is the object: what does the student write? The student walks to school. To school tells the place: where does the student walk?"},"adjectives":{"src":"lesson-images/simple_present-adjectives-2f80e109f580.webp","width":1555,"height":655,"alt":"Describe a noun. An adjective describes a noun and goes before it. The student writes a letter. Add young: The young student writes a letter. Young describes student. Keep the subject and verb."},"adverbs":{"src":"lesson-images/simple_present-adverbs-373edc79bb18.webp","width":1555,"height":800,"alt":"Describe the action. An adverb tells how. The careful student writes a letter carefully. Careful is an adjective describing student. Carefully is an adverb describing writes. Careful becomes carefully; fast stays fast."},"time":{"src":"lesson-images/simple_present-time-3f6c9391c2dd.webp","width":1555,"height":850,"alt":"Add time. A time phrase tells when. Time at the end: The young student writes a letter carefully after lunch. Time at the beginning: After lunch, the young student writes a letter carefully. Use a comma after the opening time phrase. Keep the same meaning and one final period."},"joining":{"src":"lesson-images/simple_present-joining-22acfef47e08.webp","width":1555,"height":1000,"alt":"Join ideas. Each idea has its own subject and verb. The young boy sings happily in the classroom after lunch, and the girls dance in the classroom. And adds, but contrasts, because or since gives a reason, and so shows a result. Use a comma before and, but or so when joining complete ideas. An opening reason also needs a comma: Because the teacher plays music, the students dance. One sentence, one final period."}},"present_continuous":{"simple":{"src":"lesson-images/present_continuous-simple-9a7179cc8f32.webp","width":1555,"height":655,"alt":"Simple sentences. Present continuous describes an action happening now. Subject plus am/is/are plus an -ing verb. The dancer is dancing. The dancers are dancing. I uses am; he, she or one person uses is; you, we, they or more than one uses are. Start with a capital letter and end with a period."},"details":{"src":"lesson-images/present_continuous-details-205bc6a52969.webp","width":1555,"height":655,"alt":"Add an object or place. Keep am/is/are plus an -ing verb. An object tells what receives the action: The artist is painting a picture. A place tells where the action happens: The visitors are walking to the park. Painting a picture answers what; to the park answers where."},"adjectives":{"src":"lesson-images/present_continuous-adjectives-36c5dce36f8a.webp","width":1555,"height":655,"alt":"Describe a noun. An adjective describes a noun and goes before it. The helpful neighbor is carrying a bag. Helpful is an adjective; neighbor is a noun. The present continuous verb phrase is is carrying. Keep is before carrying."},"adverbs":{"src":"lesson-images/present_continuous-adverbs-ab5fffa7e9a4.webp","width":1555,"height":800,"alt":"Describe the action. An adverb tells how. The happy singers are singing softly in the classroom. Happy describes the singers and is an adjective. Softly describes how they are singing and is an adverb. Are singing is the present continuous verb phrase. Many adverbs end in -ly: soft becomes softly."},"time":{"src":"lesson-images/present_continuous-time-9d1df2a649b4.webp","width":1555,"height":850,"alt":"Add time. A time phrase tells when. The eager players are playing ball happily at the moment. Or put time first: At the moment, the eager players are playing ball happily. Use a comma after an opening time phrase. Eager is an adjective; happily is an adverb; at is a preposition, the a determiner and moment a noun. The verb phrase remains are playing."},"joining":{"src":"lesson-images/present_continuous-joining-d8a7f5cdce4e.webp","width":1555,"height":1000,"alt":"Join ideas. Each idea has its own subject and present continuous verb phrase. The careful teacher is painting a picture slowly this morning, and the students are painting pictures. And adds, but contrasts, because or since gives a reason, and so shows a result. Use a comma before and, but or so when joining complete ideas. An opening reason also needs a comma: Because the teacher is playing music, the friends are dancing. One sentence, one final period."}},"simple_past":{"simple":{"src":"lesson-images/simple_past-simple-a1d4f4827fdc.webp","width":1555,"height":655,"alt":"Simple Sentences. Simple past describes a finished action. Use the past verb for every subject. Regular verbs often end in -ed; irregular verbs change, such as eat to ate and do to did. A sentence needs a subject and an action. Start with a capital letter and end with a period. Examples: The friends laughed."},"details":{"src":"lesson-images/simple_past-details-7c799fb900ef.webp","width":1555,"height":750,"alt":"Add an Object or Place. Simple past describes a finished action. Use the past verb for every subject. Regular verbs often end in -ed; irregular verbs change, such as eat to ate and do to did. An object tells what receives the action. A place tells where it happens. Keep the complete verb phrase together. Examples: The child ate an apple. The neighbors talked in the library."},"adjectives":{"src":"lesson-images/simple_past-adjectives-01f58f2c81ff.webp","width":1555,"height":655,"alt":"Describe a Noun. Simple past describes a finished action. Use the past verb for every subject. Regular verbs often end in -ed; irregular verbs change, such as eat to ate and do to did. An adjective describes a noun and goes before it. Keep the subject and complete verb phrase. Examples: The curious visitor opened a door."},"adverbs":{"src":"lesson-images/simple_past-adverbs-5fe8505713e2.webp","width":1555,"height":800,"alt":"Describe the Action. Simple past describes a finished action. Use the past verb for every subject. Regular verbs often end in -ed; irregular verbs change, such as eat to ate and do to did. An adjective describes a noun; an adverb describes how an action happens. Many adverbs end in -ly. Keep the complete verb phrase. Examples: The proud student did homework neatly."},"time":{"src":"lesson-images/simple_past-time-21c642da9558.webp","width":1555,"height":900,"alt":"Add Time. Simple past describes a finished action. Use the past verb for every subject. Regular verbs often end in -ed; irregular verbs change, such as eat to ate and do to did. A time phrase tells when. Time can go at the end or the beginning. Use a comma after an opening time phrase. Keep the same meaning and one final period. Examples: The friendly neighbors talked quietly in the park before lunch. Before lunch, the friendly neighbors talked quietly in the park."},"joining":{"src":"lesson-images/simple_past-joining-537cb01dcd76.webp","width":1555,"height":1100,"alt":"Join Ideas. Simple past describes a finished action. Use the past verb for every subject. Regular verbs often end in -ed; irregular verbs change, such as eat to ate and do to did. Each complete idea has its own subject and verb phrase. And adds a related idea; but contrasts; because or since gives a reason; so shows a result. Use a comma before and, but or so when joining complete ideas. Because or since in the middle needs no comma; an opening reason needs a comma after the whole reason. One sentence, one final period. Each idea has its own row and brace, excluding the joining word and punctuation. Examples: The helpful students cleaned a room carefully after school, and the worker carried bags."}},"past_continuous":{"simple":{"src":"lesson-images/past_continuous-simple-8fc1ab86ac9e.webp","width":1555,"height":700,"alt":"Simple Sentences. Past continuous describes an action in progress in the past. Use was or were plus an -ing verb. I, he, she or one person uses was; you, we, they or more than one uses were. A sentence needs a subject and an action. Start with a capital letter and end with a period. Examples: The worker was resting."},"details":{"src":"lesson-images/past_continuous-details-bf2b35410a4e.webp","width":1555,"height":800,"alt":"Add an Object or Place. Past continuous describes an action in progress in the past. Use was or were plus an -ing verb. I, he, she or one person uses was; you, we, they or more than one uses were. An object tells what receives the action. A place tells where it happens. Keep the complete verb phrase together. Examples: The artist was painting a picture. The friends were running to the park."},"adjectives":{"src":"lesson-images/past_continuous-adjectives-3e5eafcb134b.webp","width":1555,"height":700,"alt":"Describe a Noun. Past continuous describes an action in progress in the past. Use was or were plus an -ing verb. I, he, she or one person uses was; you, we, they or more than one uses were. An adjective describes a noun and goes before it. Keep the subject and complete verb phrase. Examples: The hungry children were eating lunch."},"adverbs":{"src":"lesson-images/past_continuous-adverbs-4d463eefe6c1.webp","width":1555,"height":850,"alt":"Describe the Action. Past continuous describes an action in progress in the past. Use was or were plus an -ing verb. I, he, she or one person uses was; you, we, they or more than one uses were. An adjective describes a noun; an adverb describes how an action happens. Many adverbs end in -ly. Keep the complete verb phrase. Examples: The patient visitor was waiting calmly at school."},"time":{"src":"lesson-images/past_continuous-time-96ba261fd575.webp","width":1555,"height":950,"alt":"Add Time. Past continuous describes an action in progress in the past. Use was or were plus an -ing verb. I, he, she or one person uses was; you, we, they or more than one uses were. A time phrase tells when. Time can go at the end or the beginning. Use a comma after an opening time phrase. Keep the same meaning and one final period. Examples: The careful girl was opening a door slowly before lunch. Before lunch, the careful girl was opening a door slowly."},"joining":{"src":"lesson-images/past_continuous-joining-14d38c8f0667.webp","width":1555,"height":1150,"alt":"Join Ideas. Past continuous describes an action in progress in the past. Use was or were plus an -ing verb. I, he, she or one person uses was; you, we, they or more than one uses were. Each complete idea has its own subject and verb phrase. And adds a related idea; but contrasts; because or since gives a reason; so shows a result. Use a comma before and, but or so when joining complete ideas. Because or since in the middle needs no comma; an opening reason needs a comma after the whole reason. One sentence, one final period. Each idea has its own row and brace, excluding the joining word and punctuation. Examples: The eager player was kicking a ball carefully after school, and the friends were running in the playground."}},"simple_future":{"simple":{"src":"lesson-images/simple_future-simple-097a9b9d2d7b.webp","width":1555,"height":700,"alt":"Simple Sentences. Simple future describes an action that will happen later. Use will plus the root verb. The verb stays the same for every subject. A sentence needs a subject and an action. Start with a capital letter and end with a period. Examples: The children will run."},"details":{"src":"lesson-images/simple_future-details-a2ee1e7fd75b.webp","width":1555,"height":800,"alt":"Add an Object or Place. Simple future describes an action that will happen later. Use will plus the root verb. The verb stays the same for every subject. An object tells what receives the action. A place tells where it happens. Keep the complete verb phrase together. Examples: The teacher will carry books. The children will walk to school."},"adjectives":{"src":"lesson-images/simple_future-adjectives-efa87de2c33f.webp","width":1555,"height":700,"alt":"Describe a Noun. Simple future describes an action that will happen later. Use will plus the root verb. The verb stays the same for every subject. An adjective describes a noun and goes before it. Keep the subject and complete verb phrase. Examples: The proud girl will play music."},"adverbs":{"src":"lesson-images/simple_future-adverbs-77c99393db96.webp","width":1555,"height":850,"alt":"Describe the Action. Simple future describes an action that will happen later. Use will plus the root verb. The verb stays the same for every subject. An adjective describes a noun; an adverb describes how an action happens. Many adverbs end in -ly. Keep the complete verb phrase. Examples: The kind teacher will talk softly in the classroom."},"time":{"src":"lesson-images/simple_future-time-73bd6200776a.webp","width":1555,"height":950,"alt":"Add Time. Simple future describes an action that will happen later. Use will plus the root verb. The verb stays the same for every subject. A time phrase tells when. Time can go at the end or the beginning. Use a comma after an opening time phrase. Keep the same meaning and one final period. Examples: The excited children will play ball happily tomorrow. Tomorrow, the excited children will play ball happily."},"joining":{"src":"lesson-images/simple_future-joining-024d20f608e1.webp","width":1555,"height":1150,"alt":"Join Ideas. Simple future describes an action that will happen later. Use will plus the root verb. The verb stays the same for every subject. Each complete idea has its own subject and verb phrase. And adds a related idea; but contrasts; because or since gives a reason; so shows a result. Use a comma before and, but or so when joining complete ideas. Because or since in the middle needs no comma; an opening reason needs a comma after the whole reason. One sentence, one final period. Each idea has its own row and brace, excluding the joining word and punctuation. Examples: The eager students will dance happily in the classroom tomorrow because the teacher will play music."}},"future_continuous":{"simple":{"src":"lesson-images/future_continuous-simple-d502816306ee.webp","width":1555,"height":750,"alt":"Simple Sentences. Future continuous describes an action that will be in progress at a future time. Use will plus be plus an -ing verb. Keep will, be and the -ing verb together. A sentence needs a subject and an action. Start with a capital letter and end with a period. Examples: The students will be working."},"details":{"src":"lesson-images/future_continuous-details-61a6c6e3d49e.webp","width":1555,"height":850,"alt":"Add an Object or Place. Future continuous describes an action that will be in progress at a future time. Use will plus be plus an -ing verb. Keep will, be and the -ing verb together. An object tells what receives the action. A place tells where it happens. Keep the complete verb phrase together. Examples: The girl will be cleaning a room. The boys will be resting in the park."},"adjectives":{"src":"lesson-images/future_continuous-adjectives-f1093f5c73f3.webp","width":1555,"height":750,"alt":"Describe a Noun. Future continuous describes an action that will be in progress at a future time. Use will plus be plus an -ing verb. Keep will, be and the -ing verb together. An adjective describes a noun and goes before it. Keep the subject and complete verb phrase. Examples: The eager girl will be running to school."},"adverbs":{"src":"lesson-images/future_continuous-adverbs-0fcd8a8141f8.webp","width":1555,"height":900,"alt":"Describe the Action. Future continuous describes an action that will be in progress at a future time. Use will plus be plus an -ing verb. Keep will, be and the -ing verb together. An adjective describes a noun; an adverb describes how an action happens. Many adverbs end in -ly. Keep the complete verb phrase. Examples: The kind teacher will be singing softly in the classroom."},"time":{"src":"lesson-images/future_continuous-time-39efdbddf734.webp","width":1555,"height":1000,"alt":"Add Time. Future continuous describes an action that will be in progress at a future time. Use will plus be plus an -ing verb. Keep will, be and the -ing verb together. A time phrase tells when. Time can go at the end or the beginning. Use a comma after an opening time phrase. Keep the same meaning and one final period. Examples: The busy teachers will be working carefully in the library next week. Next week, the busy teachers will be working carefully in the library."},"joining":{"src":"lesson-images/future_continuous-joining-dcbfd3e993c1.webp","width":1555,"height":1200,"alt":"Join Ideas. Future continuous describes an action that will be in progress at a future time. Use will plus be plus an -ing verb. Keep will, be and the -ing verb together. Each complete idea has its own subject and verb phrase. And adds a related idea; but contrasts; because or since gives a reason; so shows a result. Use a comma before and, but or so when joining complete ideas. Because or since in the middle needs no comma; an opening reason needs a comma after the whole reason. One sentence, one final period. Each idea has its own row and brace, excluding the joining word and punctuation. Examples: The eager players will be playing ball happily in the park tomorrow, but the tired boy will be resting."}}};
const DATA = (() => {
const nouns = [['child','children'],['student','students'],['teacher','teachers'],['friend','friends'],['player','players'],['dancer','dancers'],['singer','singers'],['neighbor','neighbors'],['visitor','visitors'],['worker','workers'],['artist','artists'],['classmate','classmates'],['boy','boys'],['girl','girls']];
const verbs = [['walk','walked','walking'],['run','ran','running'],['play','played','playing'],['dance','danced','dancing'],['sing','sang','singing'],['laugh','laughed','laughing'],['rest','rested','resting'],['wait','waited','waiting'],['talk','talked','talking'],['work','worked','working']];
const pronouns = {i:{person:1,number:'singular'},you:{person:2,number:'either'},he:{person:3,number:'singular'},she:{person:3,number:'singular'},we:{person:1,number:'plural'},they:{person:3,number:'plural'}};
const objects = {me:'i',us:'we',him:'he',her:'she',them:'they'};
const determiners = ['the','a','an','my','our','her','their','his','your','this','these'];
const subjectAdjectives = ['happy','tired','hungry','young','friendly','careful','kind','brave','calm','curious','helpful','patient','proud','polite','strong','busy','sleepy','eager','cheerful','excited'];
const adverbs=[['careful','carefully'],['slow','slowly'],['quiet','quietly'],['quick','quickly'],['happy','happily'],['loud','loudly'],['fast','fast'],['patient','patiently'],['polite','politely'],['gentle','gently'],['neat','neatly'],['clear','clearly'],['soft','softly'],['calm','calmly'],['eager','eagerly'],['brave','bravely']];
const adjectives=[...new Set([...subjectAdjectives,...adverbs.map(([root])=>root)])];
const things=[['lunch',null],['homework',null],['music',null],['book','books'],['bag','bags'],['letter','letters'],['story','stories'],['room','rooms'],['picture','pictures'],['ball','balls'],['door','doors'],['apple','apples']];
const objectVerbs={eat:['lunch','apple'],do:['homework'],write:['letter','story'],carry:['book','bag'],clean:['room'],paint:['picture'],kick:['ball'],open:['door'],play:['music','ball']};
verbs.push(['eat','ate','eating'],['do','did','doing','does'],['write','wrote','writing'],['carry','carried','carrying','carries'],['clean','cleaned','cleaning'],['paint','painted','painting'],['kick','kicked','kicking'],['open','opened','opening']);
const places = ['school','home','classroom','library','park','playground','bedroom'];
const pastTimes=['after school','before school','after lunch','before lunch','in the morning','in the afternoon','in the evening','on monday','on tuesday','on friday'];
const chapters=[
{tense:'simple_present',title:'Simple present',pattern:'present verb',rule:'Use the root verb with I/you/we/they or plural subjects. With he/she or one person, add -s: play → plays; do → does.',times:['after school','before school','after lunch','before lunch','in the morning','in the afternoon','in the evening','every day','every week','every monday']},
{tense:'present_continuous',title:'Present continuous',pattern:'am/is/are + -ing verb',rule:'Use am/is/are + an -ing verb. Am: I; is: he/she or one person; are: you/we/they or more than one.',times:['now','right now','at the moment','this morning','this afternoon','this evening','today','after school','before lunch','in the morning']},
{tense:'simple_past',title:'Simple past',pattern:'past verb',rule:'Use a past verb: play → played; eat → ate.',times:pastTimes},
{tense:'past_continuous',title:'Past continuous',pattern:'was/were + -ing verb',rule:'Was/were + an -ing verb. Was: I/he/she or one person; were: you/we/they or more than one.',times:pastTimes},
{tense:'simple_future',title:'Simple future',pattern:'will + root verb',rule:'Use will + the root verb: will play; will eat. The verb stays the same for every subject.',times:['tomorrow','tomorrow morning','tomorrow afternoon','tomorrow evening','next week','on monday','on tuesday','on friday','after school','before lunch']},
{tense:'future_continuous',title:'Future continuous',pattern:'will be + -ing verb',rule:'Use will + be + an -ing verb: will be playing; will be eating. Keep be after will for every subject.',times:['tomorrow','tomorrow morning','tomorrow afternoon','tomorrow evening','next week','on monday','on tuesday','on friday','after school','before lunch']}
];
const times=[...new Set(chapters.flatMap(c=>c.times))].map(x=>x.split(' ')).sort((a,b)=>b.length-a.length);
const lexicon = Object.create(null);
function add(word,role,features={}) {
if (!lexicon[word]) lexicon[word]={word,initialSound:/^(artist|apple|open|eager|excited)/.test(word)?'vowel':'consonant',roles:[]};
lexicon[word].roles.push({role,...features});
}
for (const [lemma,plural] of nouns) {add(lemma,'people',{lemma,number:'singular'});add(plural,'people',{lemma,number:'plural'});}
for (const [word,features] of Object.entries(pronouns)) add(word,'subject_pronoun',features);
for (const [word,replacement] of Object.entries(objects)) add(word,'object_pronoun',{replacement});
determiners.forEach(x=>add(x,'determiner'));
adjectives.forEach(x=>add(x,'adjective'));
adverbs.forEach(([lemma,word])=>add(word,'adverb',{lemma}));
things.forEach(([lemma,plural])=>{add(lemma,'thing',{lemma,number:plural?'singular':lemma==='lunch'?'flexible':'mass'});if(plural)add(plural,'thing',{lemma,number:'plural'});});
['because','since','so','and','but'].forEach(x=>add(x,'conjunction'));
// Present-s spellings use authored exceptions (does/carries) or the regular +s form.
for (const [lemma,past,ing,present=lemma+'s'] of verbs) {add(lemma,'verb',{lemma,past,form:'base',finite:true});add(past,'verb',{lemma,past,form:'past',finite:true});add(ing,'verb',{lemma,past,form:'ing',finite:false});add(present,'verb',{lemma,past,form:'present_s',finite:true});}
for(const word of ['be','was','were','am','is','are'])add(word,'auxiliary',{lemma:'be',finite:word!=='be',tense:['am','is','are'].includes(word)?'present':word==='be'?null:'past'});
add('will','auxiliary',{lemma:'will',finite:true,tense:'future'});
for (const word of ['after','before','school','lunch','in','morning','afternoon','evening','on','monday','tuesday','friday','at','home','classroom','library','park','playground','to']) add(word,'phrase_word');
for(const word of ['now','right','moment','today','tomorrow','next','week','day','every'])add(word,'phrase_word');
for (const place of places) add(place,'place',{number:'singular'});
// Teaching labels describe the supported uses, independently of parser roles.
const partsOfSpeech={noun:{label:'Noun',short:'N'},verb:{label:'Verb',short:'V'},pronoun:{label:'Pronoun',short:'PRO'},determiner:{label:'Determiner',short:'DET'},adjective:{label:'Adjective',short:'ADJ'},preposition:{label:'Preposition',short:'PREP'},adverb:{label:'Adverb',short:'ADV'},conjunction:{label:'Conjunction',short:'CONJ'},punctuation:{label:'Punctuation',short:'PUNC'}};
const roleParts={people:'noun',thing:'noun',place:'noun',verb:'verb',auxiliary:'verb',subject_pronoun:'pronoun',object_pronoun:'pronoun',determiner:'determiner',adjective:'adjective',adverb:'adverb',conjunction:'conjunction'};
for(const entry of Object.values(lexicon)){
const tags=entry.roles.map(r=>roleParts[r.role]||(r.role==='phrase_word'?(['after','before','in','on','at','to'].includes(entry.word)?'preposition':'noun'):null)).filter(Boolean);
if(entry.word==='home')tags.push('adverb');
if(['now','right','today','tomorrow'].includes(entry.word))tags.splice(0,tags.length,'adverb');
if(entry.word==='every')tags.splice(0,tags.length,'determiner');
if(entry.word==='next')tags.splice(0,tags.length,'adjective');
entry.partsOfSpeech=[...new Set(tags)];
}
const lessons=[{id:'simple',title:'Simple sentences'},{id:'details',title:'Add an object or place'},{id:'adjectives',title:'Describe a noun'},{id:'adverbs',title:'Describe the action'},{id:'time',title:'Add time'},{id:'joining',title:'Join ideas'},{id:'review',title:'Review / quiz'}];
const freeze = value => {if(value && typeof value==='object'){Object.values(value).forEach(freeze);Object.freeze(value);}return value;};
return freeze({version:VERSION,nouns,verbs,pronouns,objects,determiners,adjectives,subjectAdjectives,adverbs,things,objectVerbs,places,times,lexicon,partsOfSpeech,chapters,lessons,curriculumLimits:{words:26,punctuation:4,total:30},limits:{words:12,punctuation:4,total:16,analyses:128,diagnostics:256}});
})();
/* ENGINE — pure, bounded checking. No DOM, network, storage, or clock reads. */
const ENGINE = (() => {
const own = (o,k) => Object.prototype.hasOwnProperty.call(o,k);
const cap = s => s[0].toUpperCase()+s.slice(1);
const quote = s => `“${s}”`;
const quotedEnd = s => `“${s}.”`;
const equal = (a,b) => JSON.stringify(a)===JSON.stringify(b);
const finding = (code,kind,message,highlights=[],anchors=[]) => ({code,kind,message,highlights:[...new Set(highlights)].sort((a,b)=>a-b),anchors});
const roles = word => DATA.lexicon[word]?.roles || [];
const role = (word,name) => roles(word).find(x=>x.role===name);
const tokenize = text => typeof text==='string' ? (text.match(/[A-Za-z]+|[^\s]/g)||[]) : [];
function createBudget(overrides,curriculum=false) {
const limits={...DATA.limits,...(curriculum?DATA.curriculumLimits:{})};
if (overrides && (typeof overrides!=='object'||Array.isArray(overrides))) throw {reason:'invalid_limits'};
for (const [key,value] of Object.entries(overrides||{})) {
if(!own(limits,key)||!Number.isInteger(value)||value<0||value>limits[key]) throw {reason:'invalid_limits'};
limits[key]=value;
}
return {limits,analyses:0,diagnostics:0,curriculum};
}
function tick(b,key) {if(b[key]>=b.limits[key]) throw {reason:key==='analyses'?'analysis_budget':'diagnostic_budget'};b[key]++;}
function projection(input,b) {
if(!Array.isArray(input)) throw {reason:'invalid_input'};
if(input.length>b.limits.total) throw {reason:'token_limit'};
const raw=input.map((x,i)=> typeof x==='string'?{id:i,text:x,index:i}:x && typeof x==='object' && typeof x.text==='string'?{id:x.id??i,text:x.text,index:i}:null);
if(raw.some(x=>!x||typeof x.id!=='number'&&typeof x.id!=='string'||x.text.length>80) || new Set(raw.map(x=>x.id)).size!==raw.length) throw {reason:'invalid_input'};
const words=raw.filter(x=>x.text!==','&&x.text!=='.').map(x=>({...x,lower:x.text.toLowerCase()}));
const punctuation=raw.filter(x=>x.text===','||x.text==='.');
if(words.length>b.limits.words) throw {reason:'word_limit'};
if(punctuation.length>b.limits.punctuation) throw {reason:'punctuation_limit'};
if(words.some(x=>!own(DATA.lexicon,x.lower))) throw {reason:'unavailable_word'};
return {raw,words,punctuation};
}
function part(words,start,end,extra={}) {return {words:words.slice(start,end).map(x=>x.lower),indices:words.slice(start,end).map(x=>x.index),end,...extra};}
function nounPhrase(words,start,type,relaxed,bare=false) {
let p=start; const det=DATA.determiners.includes(words[p]?.lower)?words[p++]:null;
const adj=DATA.adjectives.includes(words[p]?.lower)?words[p++]:null;
const head=words[p]; if(!head) return [];
const noun=role(head.lower,type); if(!noun) return [];
const errors=[];
if(!det && noun.number==='singular' && !bare) errors.push(finding(type==='people'?'subject_determiner':type==='thing'?'object_determiner':'place_determiner','grammar',type==='people'?`Add a determiner before the singular noun ${quotedEnd(head.lower)}`:`Add a determiner before the ${type==='thing'?'object':'place'} noun ${quotedEnd(head.lower)}`,[],[{before:head.index}]));
if(det) {
const mismatch=noun.number==='plural'&&['a','an','this'].includes(det.lower)||noun.number==='singular'&&det.lower==='these'||noun.number==='mass'&&['a','an','these'].includes(det.lower);
if(mismatch) {
const replacement=noun.number==='singular'?'this':noun.number==='mass'?'the':det.lower==='this'?'these':'the';
errors.push(finding('determiner_number','grammar',`Use ${quote(replacement)} with the ${noun.number} noun ${quotedEnd(head.lower)}`,[det.index]));
}
if(['a','an'].includes(det.lower)) {
const next=adj||head, expected=DATA.lexicon[next.lower].initialSound==='vowel'?'an':'a';
if(det.lower!==expected) errors.push(finding('article','grammar',`Change ${quote(det.lower)} to ${quote(expected)} before ${quotedEnd(next.lower)}`,[det.index]));
}
}
if(errors.length&&!relaxed) return [];
return [part(words,start,p+1,{head:head.lower,headIndex:head.index,lemma:noun.lemma||head.lower,number:noun.number,errors})];
}
function subjects(words,start,relaxed) {
const out=[];const token=words[start];if(!token)return out;
if(own(DATA.pronouns,token.lower)) out.push(part(words,start,start+1,{head:token.lower,headIndex:token.index,lemma:token.lower,number:DATA.pronouns[token.lower].number,errors:[]}));
if(relaxed && own(DATA.objects,token.lower)) {
const replacement=DATA.objects[token.lower];
out.push(part(words,start,start+1,{head:token.lower,headIndex:token.index,lemma:replacement,number:DATA.pronouns[replacement].number,errors:[finding('subject_case','grammar',`Use ${quote(replacement==='i'?'I':replacement)} as the subject instead of ${quotedEnd(token.lower)}`,[token.index])]}));
}
return out.concat(nounPhrase(words,start,'people',relaxed));
}
function verbWords(subject,lemma,tense){
const v=DATA.verbs.find(v=>v[0]===lemma);if(!v)throw Error('Unknown action');
const head=subject?.lemma||subject?.head,plural=head==='you'||subject?.number==='plural',third=!plural&&!['i','we'].includes(head);
const presentAux=head==='i'?'am':plural?'are':'is',pastAux=plural?'were':'was';
const forms={simple_present:[third?v[3]||v[0]+'s':v[0]],present_continuous:[presentAux,v[2]],simple_past:[v[1]],past_continuous:[pastAux,v[2]],simple_future:['will',v[0]],future_continuous:['will','be',v[2]]};
if(!forms[tense])throw Error('Unknown tense');return forms[tense];
}
function action(words,start,relaxed,tense=null,subject=null) {
const token=words[start],aux=role(token?.lower,'auxiliary'),future=aux?.lemma==='will';
let offset=aux?1:0;
// Bounded auxiliary chains let students mix known forms without losing the whole sentence.
while(offset<3&&role(words[start+offset]?.lower,'auxiliary'))offset++;
const verbToken=words[start+offset],v=role(verbToken?.lower,'verb');if(!v)return null;
const target=tense||'simple_past',continuous=target==='past_continuous',futureContinuous=future&&offset>1;
const requested=verbWords(subject,v.lemma,target),expectedAux=verbWords(subject,v.lemma,'past_continuous')[0],ing=DATA.verbs.find(x=>x[0]===v.lemma)[2];
const phrase=words.slice(start,start+offset+1),errors=[];
const actual=future?(futureContinuous?'future_continuous':'simple_future'):aux?(aux.tense==='present'?'present_continuous':'past_continuous'):v.form==='past'?'simple_past':v.form==='ing'?null:'simple_present';
if(target==='simple_past'||continuous){
// Preserve reviewed past-lesson diagnostics, shared by all parser callers.
if(future){
errors.push(finding('tense_choice','task',`This challenge needs the ${continuous?'past continuous':'simple past'} tense. Use ${quote(requested.join(' '))} instead of ${quotedEnd(phrase.map(w=>w.lower).join(' '))}`,phrase.map(w=>w.index)));
}else if(aux){
if(!continuous)errors.push(finding('tense_choice','task',`${v.form==='ing'?'This challenge needs the simple past tense.':'For this simple-past activity,'} ${v.form==='ing'?'Use':'use'} ${quote(v.past)} without ${quotedEnd(token.lower)}`,[token.index,verbToken.index]));
else{
if(aux.tense==='present')errors.push(finding('tense_choice','task',`This challenge needs the past continuous tense. Use ${quote(expectedAux)} instead of ${quotedEnd(token.lower)}`,[token.index]));
const agreement=verbWords(subject,v.lemma,aux.tense==='present'?'present_continuous':'past_continuous')[0];
if(token.lower==='be'||subject&&token.lower!==agreement)errors.push(finding('auxiliary_agreement','grammar',aux.tense==='present'?`Use ${quote(expectedAux)} with this subject for the past continuous tense.`:`Use ${quote(expectedAux)} with this subject instead of ${quotedEnd(token.lower)}`,[token.index]));
if(v.form!=='ing')errors.push(finding('continuous_form','grammar',`The past continuous tense uses was/were + an -ing action. Change ${quote(verbToken.lower)} to ${quotedEnd(ing)}`,[verbToken.index]));
}
}else if(continuous){
errors.push(v.form==='ing'?finding('missing_auxiliary','grammar',`For the past continuous tense, add ${quote(expectedAux)} before ${quotedEnd(ing)}`,[],[{before:verbToken.index}]):finding('tense_choice','task',`This challenge needs the past continuous tense. Use ${quote(requested.join(' '))} instead of ${quotedEnd(verbToken.lower)}`,[verbToken.index]));
}else if(v.form!=='past')errors.push(v.form==='ing'?finding('finite_action','grammar',`Use a ${tense?'simple past-tense':'past-tense'} action, such as ${quotedEnd(v.past)}`,[token.index]):finding('past_tense','grammar',`This challenge needs ${tense?'the simple past tense':'past tense'}. Change ${quote(token.lower)} to ${quotedEnd(v.past)}`,[token.index]));
}else{
const name=DATA.chapters.find(c=>c.tense===target).title.toLowerCase();
if(actual!==target)errors.push(finding('tense_choice','task',`This challenge needs the ${name} tense. Use ${quote(requested.join(' '))} instead of ${quotedEnd(phrase.map(w=>w.lower).join(' '))}`,phrase.map(w=>w.index)));
if(actual===target&&target==='simple_present'&&subject&&verbToken.lower!==requested[0])errors.push(finding('auxiliary_agreement','grammar',`Use ${quote(requested[0])} with this subject for the simple present tense.`,[verbToken.index]));
if(target==='present_continuous'&&!aux&&v.form==='ing')errors.push(finding('missing_auxiliary','grammar',`Add ${quote(requested[0])} before ${quotedEnd(ing)}`,[],[{before:verbToken.index}]));
}
if(future){
if(futureContinuous&&(offset!==2||words[start+1].lower!=='be'))errors.push(finding('future_form','grammar','Use will be + an -ing action; keep the root be after will.',phrase.slice(0,-1).map(w=>w.index)));
if(v.form!==(futureContinuous?'ing':'base'))errors.push(finding('future_form','grammar',futureContinuous?'Will be needs an -ing action in this pattern.':v.form==='ing'?'Add be after will before an -ing action, or use the root action after will.':'Will needs the root action in this pattern.',[verbToken.index]));
}else if(aux&&target!=='past_continuous'){
const agreement=verbWords(subject,v.lemma,aux.tense==='present'?'present_continuous':'past_continuous')[0];
if(subject&&token.lower!==agreement)errors.push(finding('auxiliary_agreement','grammar',`Use ${quote(agreement)} with this subject instead of ${quotedEnd(token.lower)}`,[token.index]));
if(v.form!=='ing')errors.push(finding('continuous_form','grammar',`Use an -ing action after ${quote(agreement)}: ${quotedEnd(ing)}`,[verbToken.index]));
}
if(!future&&offset>1)errors.push(finding('continuous_form','grammar','Use one agreeing form of be before the -ing action.',phrase.slice(0,-1).map(w=>w.index)));
if(errors.length&&!relaxed)return null;
return part(words,start,start+offset+1,{lemma:v.lemma,past:v.past,finite:aux?aux.finite:v.finite,tense:actual,errors});
}
function time(words,start) {
for(const phrase of DATA.times) if(phrase.every((x,i)=>words[start+i]?.lower===x)) return part(words,start,start+phrase.length,{errors:[]});
return null;
}
function locations(words,start,v,relaxed) {
const out=[{end:start,value:null}]; const token=words[start];if(!token)return out;
const motion=['walk','run'].includes(v.lemma);
if(token.lower==='home') {
const item=part(words,start,start+1,{head:'home',npIndices:[token.index],errors:[],taskErrors:[]});
if(!motion) item.taskErrors.push(finding('location_action','task',v.tense&&!['simple_past','past_continuous'].includes(v.tense)?'For this activity, use “home” with a form of walk or run.':v.tense==='past_continuous'?'For this activity, use “home” with “was/were walking” or “was/were running.”':'For this activity, use “home” with “walked” or “ran.”',[...v.indices,token.index]));
out.push({end:item.end,value:item});return out;
}
if(!['at','in','on','to'].includes(token.lower))return out;
const next=words[start+1]?.lower;
// to home is deliberately outside the contract; do not guess a missing article.
if(token.lower==='to'&&next==='home')return out;
const bare=!DATA.determiners.includes(next)&&!DATA.adjectives.includes(next) && (next==='school'&&['at','in','to'].includes(token.lower)||next==='home'&&token.lower==='at');
for(const np of nounPhrase(words,start+1,'place',relaxed,bare)) {
if(token.lower==='on'&&np.head!=='playground')continue;
const item=part(words,start,np.end,{head:np.head,npIndices:np.indices,errors:np.errors,taskErrors:[]});
if(token.lower==='to'&&!motion) item.taskErrors.push(finding('location_action','task',`For this activity, use ${quote(item.words.join(' '))} with ${v.tense&&!['simple_past','past_continuous'].includes(v.tense)?'a form of walk or run.':v.tense==='past_continuous'?'“was/were walking” or “was/were running.”':'“walked” or “ran.”'}`,[...v.indices,...item.indices]));
out.push({end:item.end,value:item});
}
return out;
}
function parseAll(words,relaxed,b,tense) {
const results=[];
for(const template of ['A','B','ALT','NO_TIME']) {
tick(b,'analyses');if(relaxed)tick(b,'diagnostics');
const firstTime=template==='B'?time(words,0):null;
if(template==='B'&&!firstTime)continue;
const start=firstTime?.end||0;
for(const s of subjects(words,start,relaxed)) {
const v=action(words,s.end,relaxed,tense,s);if(!v)continue;
if(template==='ALT') {
const t=time(words,v.end);if(!t)continue;
for(const l of locations(words,t.end,v,relaxed)) if(l.value&&l.end===words.length) results.push(make(s,v,l.value,t,template));
} else {
for(const l of locations(words,v.end,v,relaxed)) {
const t=template==='B'?firstTime:template==='A'?time(words,l.end):null;
const end=template==='A'?t?.end:l.end;
if(end!==words.length||template==='A'&&!t)continue;
results.push(make(s,v,l.value,t,template));
}
}
}
}
function make(s,v,l,t,template) {return {subject:s,action:v,location:l,time:t,template,grammarErrors:[...s.errors,...v.errors,...(l?.errors||[])],taskErrors:l?.taskErrors||[],repair:[]};}
return results;
}
function key(a) {return JSON.stringify(['subject','action','location','time'].map(k=>a[k]?.words||null));}
function choose(results,target) {
if(!results.length)return null;
const exact=results.filter(x=>x.template===target&&!x.grammarErrors.length&&!x.taskErrors.length);
const pool=exact.length?exact:results;
const minimum=Math.min(...pool.map(x=>x.grammarErrors.length+x.taskErrors.length));
const best=pool.filter(x=>x.grammarErrors.length+x.taskErrors.length===minimum);
if(new Set(best.map(key)).size>1)throw {reason:'conflicting_analyses'};
return best[0];
}
function partial(words,b,tense) {
tick(b,'diagnostics');
const t=time(words,0),start=t?.end||0;
const s=subjects(words,start,false)[0];
if(s && (s.end===words.length || time(words,s.end)?.end===words.length)) {
return {subject:s,action:null,location:null,time:t||time(words,s.end),template:t?'B':'A',repair:[],taskErrors:[],grammarErrors:[finding('missing_action','grammar',tense==='past_continuous'?'Add was/were + an -ing action after the subject.':tense==='simple_past'?'Add a simple past-tense action after the subject.':tense?`Add an action in the ${DATA.chapters.find(c=>c.tense===tense).title.toLowerCase()} tense after the subject.`:'Add a past-tense action after the subject.',[],[{after:s.indices.at(-1)}])]};
}
if(s&&tense==='past_continuous'&&role(words[s.end]?.lower,'auxiliary')&&(s.end+1===words.length||time(words,s.end+1)?.end===words.length))return {subject:s,action:null,location:null,time:t||time(words,s.end+1),template:t?'B':'A',repair:[],taskErrors:[],grammarErrors:[finding('missing_continuous_action','grammar','For this activity, add an -ing action after was/were.',[],[{after:words[s.end].index}])]};
if(t) {
const v=action(words,start,false,tense);
if(v && locations(words,v.end,v,false).some(l=>l.end===words.length)) return {subject:null,action:v,location:null,time:t,template:'B',repair:[],taskErrors:[],grammarErrors:[finding('missing_subject','grammar','Add a person or people before the action.',[],[{before:v.indices[0]}])]};
}
return null;
}
function analyze(words,target,b,tense) {
let result=choose(parseAll(words,false,b,tense),target);if(result)return result;
result=choose(parseAll(words,true,b,tense),target);if(result)return result;
const repairs=[];
for(let i=1;i<words.length;i++) {
if(words[i].lower!==words[i-1].lower)continue;
tick(b,'diagnostics');
const reduced=words.filter((_,j)=>j!==i);
const candidate=choose(parseAll(reduced,false,b,tense),target);
if(candidate)repairs.push({...candidate,repair:[words[i]]});
}
if(repairs.length) {
if(new Set(repairs.map(key)).size>1)throw {reason:'conflicting_analyses'};
return repairs[0];
}
return partial(words,b,tense);
}
function conventionFindings(p,a) {
const fs=[];
for(let i=0;i<p.words.length;i++) {
const w=p.words[i],proper=w.lower==='i'||['monday','tuesday','friday'].includes(w.lower),expected=proper||i===0?cap(w.lower):w.lower;
if(w.text!==expected) fs.push(finding('capitalization','convention',w.lower==='i'?'Use a capital letter for the pronoun “I.”':proper||i===0?`Use a capital letter at the start of “${expected}.”`:`Use lowercase for ${quote(w.lower)} inside this sentence.`,[w.index]));
}
const periods=p.punctuation.filter(x=>x.text==='.'),last=p.raw.at(-1);
if(periods.length!==1||last?.text!=='.') fs.push(finding('period','convention','End your sentence with a period.',periods.length>1?periods.slice(0,-1).map(x=>x.index):[],[{after:p.words.at(-1)?.index??-1}]));
const commas=p.punctuation.filter(x=>x.text===',');
const opening=a.template==='B';
if(opening) {
const anchor=a.time.indices.at(-1),next=a.subject?.indices[0]??a.action?.indices[0];
const correct=commas.filter(x=>x.index>anchor&&x.index<next);
if(!correct.length) {
const phrase=a.time.words.join(' ');
fs.push(commas.length?finding('comma_position','convention',`For this activity, move the comma after “${phrase}.”`,commas.map(x=>x.index),[{after:anchor}]):finding('comma_required','convention',`For this activity, add a comma after “${phrase}.”`,[],[{after:anchor}]));
} else {
const extra=commas.filter(x=>x!==correct[0]);
if(extra.length)fs.push(finding('extra_comma','convention','Remove the extra comma.',extra.map(x=>x.index)));
}
} else if(commas.length) fs.push(finding('extra_comma','convention',last?.text==='.'&&commas.some(x=>x.index===last.index-1)?'Remove the comma before the period.':'Remove the extra comma.',commas.map(x=>x.index)));
return fs;
}
function uncertain(reason,p,b) {
const messages={word_limit:'Use no more than 12 words in this activity.',punctuation_limit:'Use no more than four punctuation tiles in this activity.',token_limit:'Use no more than 16 tiles in this activity.',unavailable_word:'Use only words from this activity’s word bank.',analysis_budget:'I cannot check this sentence yet. Try the pattern shown above.',diagnostic_budget:'I cannot check this sentence yet. Try the pattern shown above.',unsupported_location:'I cannot check this construction yet. Try a location form shown above.',unsupported_construction:'I cannot check this construction yet. Try the pattern shown above.'};
if(b?.curriculum){messages.word_limit=`Use no more than ${b.limits.words} words in this activity.`;messages.token_limit=`Use no more than ${b.limits.total} tiles in this activity.`;}
const message=messages[reason]||'I cannot check this arrangement yet. Try the pattern shown above.';
let indices=[];
if(reason.startsWith('unsupported_')&&p) {
indices=p.raw.map(x=>x.index);
if(reason==='unsupported_location') {
const start=p.words.findIndex(x=>['at','in','on','to'].includes(x.lower));
if(start>=0 && start>0) {
const lastTime=DATA.times.map(t=>({t,start:p.words.findIndex((_,i)=>t.every((x,j)=>p.words[i+j]?.lower===x))})).filter(x=>x.start>start).sort((x,y)=>x.start-y.start)[0];
indices=p.words.slice(start,lastTime?.start??p.words.length).map(x=>x.index);
}
}
}
return {outcome:'unscored',score:null,marks:{sentence:null,conventions:null},reason,findings:[finding(reason,'uncertainty',message,indices)],feedback:message,analysis:null,stats:{analyses:b?.analyses||0,diagnostics:b?.diagnostics||0}};
}
function unknownReason(p,tense) {
if(p.words.some((w,i)=>DATA.adjectives.includes(w.lower)&&DATA.adjectives.includes(p.words[i+1]?.lower))) return 'unsupported_construction';
const t=time(p.words,0),start=t?.end||0;
for(const s of subjects(p.words,start,false)) {
const v=action(p.words,s.end,false,tense,s);
if(v && v.end<p.words.length && !time(p.words,v.end))return 'unsupported_location';
}
return 'unsupported_pattern';
}
function evaluate(input,challenge,b) {
const p=projection(input,b);
if(!p.raw.length)return {outcome:'empty',score:null,marks:{sentence:null,conventions:null},reason:null,findings:[],feedback:'Build a sentence before checking it.',analysis:null,stats:{analyses:b.analyses,diagnostics:b.diagnostics}};
if(!challenge||!['A','B'].includes(challenge.arrangement))throw {reason:'invalid_challenge'};
if(challenge.tense!=null&&!DATA.chapters.some(x=>x.tense===challenge.tense))throw {reason:'invalid_challenge'};
if(challenge.requiredTime!=null && !DATA.times.some(x=>x.join(' ')===challenge.requiredTime))throw {reason:'invalid_challenge'};
if(challenge.framework!=null){
const f=challenge.framework;
if(!f||typeof f!=='object'||Array.isArray(f)||!DATA.nouns.some(x=>x[0]===f.subject)||!DATA.verbs.some(x=>x[0]===f.action)||!DATA.places.includes(f.location))throw {reason:'invalid_challenge'};
}
const a=analyze(p.words,challenge.arrangement,b,challenge.tense);
if(!a)return uncertain(unknownReason(p,challenge.tense),p,b);
const fs=[...a.grammarErrors,...a.taskErrors];
const eligible=Boolean(a.subject&&a.action?.finite);
if(a.repair.length) {
const w=a.repair[0];
fs.push(challenge.source?finding('source_extra','task',`Use ${quote(w.lower)} once, as in your original sentence.`,[w.index]):finding('extra_component','task',`Use ${quote(w.lower)} once. Remove the extra copy.`,[w.index]));
}
if(a.subject&&a.action) {
if(!a.time) fs.push(finding('required_time','task','Add one of the time phrases shown above.',[],[{after:(a.location||a.action).indices.at(-1)}]));
else if(a.template!==challenge.arrangement) fs.push(finding('arrangement','task',`For this challenge, move “${a.time.words.join(' ')}” to the ${challenge.arrangement==='B'?'beginning':'end'}.`,a.time.indices));
if(challenge.requiredTime&&a.time&&a.time.words.join(' ')!==challenge.requiredTime)fs.push(finding('required_time','task',`For this challenge, use ${quote(challenge.requiredTime)} as your time phrase.`,a.time.indices));
if(challenge.framework){
const f=challenge.framework;
if(a.subject.lemma!==f.subject)fs.push(finding('framework_subject','task',`Use a form of ${quote(f.subject)} as your subject.`,[a.subject.headIndex]));
if(a.action.lemma!==f.action)fs.push(finding('framework_action','task',`Use a form of ${quote(f.action)} as your action.`,a.action.indices));
if(a.location?.head!==f.location)fs.push(finding('framework_location','task',`Use ${quote(f.location)} as the place in your sentence.`,a.location?.npIndices||[],a.location?[]:[{after:a.action.indices.at(-1)}]));
}
if(challenge.source) {
const src=challenge.source;
if(!src||!Array.isArray(src.tokens)||!['A','B'].includes(src.arrangement)||src.tense!=null&&!DATA.chapters.some(x=>x.tense===src.tense))throw {reason:'invalid_source'};
const original=evaluate(src.tokens,{arrangement:src.arrangement,tense:src.tense},b);
if(original.outcome!=='pass')throw {reason:'invalid_source'};
const old=original.analysis,changed=['subject','action','location','time'].filter(k=>!equal(a[k]?.words||null,old[k]?.words||null));
if(changed.length) {
const highlights=[],anchors=[];let message;
for(const k of changed) {
if(!a[k]) {anchors.push({after:a.action.indices.at(-1)});continue;}
if(k==='subject')highlights.push(...(a.subject.head!==old.subject.head?[a.subject.headIndex]:a.subject.indices));
else if(k==='location')highlights.push(...(a.location.npIndices||a.location.indices));
else highlights.push(...a[k].indices);
}
if(changed.length===2&&changed.includes('subject')&&changed.includes('location'))message=`Keep ${quote(old.subject.words.join(' '))} and ${quote(old.location.words.slice(1).join(' '))} unchanged.`;
else {
const k=changed[0],phrase=old[k]?.words.join(' ')||'';
message=k==='subject'?`Keep ${quote(phrase)} as your original subject.`:k==='action'?`Keep ${quote(phrase)} as the action from your original sentence.`:k==='location'?`Keep ${quote(phrase)} as the location from your original sentence.`:`Keep ${quote(phrase)} as the time phrase from your original sentence.`;
}
fs.push(finding('source_content','task',message,highlights,anchors));
}
// Role comparison is authoritative; also check the full normalized inventory.
const inventory=xs=>xs.filter(x=>x!==','&&x!=='.').map(x=>typeof x==='string'?x.toLowerCase():x.text.toLowerCase()).sort();
if(!equal(inventory(input),inventory(src.tokens))&&!changed.length&&!a.repair.length)throw {reason:'source_inventory'};
}
if(challenge.previous) {
const previous=challenge.previous;
if(!Array.isArray(previous.tokens)||!['A','B'].includes(previous.arrangement)||previous.tense!=null&&!DATA.chapters.some(x=>x.tense===previous.tense))throw {reason:'invalid_previous'};
const prior=evaluate(previous.tokens,{arrangement:previous.arrangement,tense:previous.tense},b);
if(prior.outcome!=='pass')throw {reason:'invalid_previous'};
const sameSubject=a.subject.lemma===prior.analysis.subject.lemma,sameAction=a.action.lemma===prior.analysis.action.lemma;
if(sameSubject&&sameAction)fs.push(finding('breadth_both','task','For this challenge, choose a different subject and action.',[...a.subject.indices,...a.action.indices]));
else if(sameSubject)fs.push(finding('breadth_subject','task','For this challenge, choose a different subject noun or pronoun.',[a.subject.headIndex]));
else if(sameAction)fs.push(finding('breadth_action','task','For this challenge, choose a different action.',a.action.indices));
}
}
if(eligible)fs.push(...conventionFindings(p,a));
const sentence=eligible&&!fs.some(x=>x.kind==='grammar'||x.kind==='task'),conventions=eligible&&!fs.some(x=>x.kind==='convention');
const score=Number(sentence)+Number(conventions);
const feedback=fs[0]?.message||(challenge.source?'You changed the arrangement and kept the sentence content.':challenge.previous?'You used a different subject and action.':challenge.arrangement==='B'?'Your time phrase comes at the beginning.':'Your time phrase comes at the end.');
return {outcome:score===2?'pass':'revise',score,marks:{sentence:Number(sentence),conventions:Number(conventions)},reason:null,findings:fs,feedback,analysis:a,stats:{analyses:b.analyses,diagnostics:b.diagnostics}};
}
function stageAdverb(words,start){
const token=words[start];if(!token)return null;
const adv=role(token.lower,'adverb'),pair=DATA.adverbs.find(x=>x[0]===token.lower);
if(!adv&&!pair)return null;
return part(words,start,start+1,{lemma:adv?.lemma||pair[0],errors:adv?[]:[finding('adverb_form','grammar',`Use “${pair[1]}” to describe the action.`,[token.index])]});
}
function stageClause(words,tense,b){
if(!words.length)return null;
const candidates=[];
for(const first of [false,true]){
tick(b,'analyses');const opening=first?time(words,0):null;if(first&&!opening)continue;
for(const s of subjects(words,opening?.end||0,true)){
const before=stageAdverb(words,s.end);
for(const pre of [null,before].filter((x,i)=>i===0||x)){
const v=action(words,pre?.end||s.end,true,tense,s);if(!v)continue;
const objects=[null,...nounPhrase(words,v.end,'thing',true,v.lemma==='play'&&words[v.end]?.lower==='ball')];
for(const obj of objects){
if(obj&&!DATA.objectVerbs[v.lemma]?.includes(obj.lemma))continue;
const after=stageAdverb(words,obj?.end||v.end);
for(const adv of pre?[pre]:[null,after].filter((x,i)=>i===0||x)){
const end=adv&&adv!==pre?adv.end:obj?.end||v.end;
for(const loc of locations(words,end,v,true)){
const finalAdv=!adv?stageAdverb(words,loc.end):null;
for(const tail of [null,finalAdv].filter((x,i)=>i===0||x)){
const position=tail?.end||loc.end,t=opening||time(words,position);
if((opening?position:t?.end||position)!==words.length)continue;
const errors=[...s.errors,...v.errors,...(obj?.errors||[]),...(loc.value?.errors||[]),...(adv?.errors||[]),...(tail?.errors||[])];
candidates.push({subject:s,action:v,object:obj,location:loc.value,adverb:adv||tail,adverbPosition:pre?'before':tail&&loc.value?'end':'after',time:t,template:opening?'B':'A',lastIndex:words.at(-1).index,grammarErrors:errors,taskErrors:loc.value?.taskErrors||[],repair:[]});
}
}
}
}
}
}
}
if(!candidates.length){const result=partial(words,b,tense);return result?{...result,lastIndex:words.at(-1).index}:null;}
const minimum=Math.min(...candidates.map(x=>x.grammarErrors.length+x.taskErrors.length));
const best=candidates.filter(x=>x.grammarErrors.length+x.taskErrors.length===minimum);
const identity=x=>JSON.stringify(['subject','action','object','adverb','location','time'].map(k=>x[k]?.words||null));
if(new Set(best.map(identity)).size>1)throw {reason:'conflicting_analyses'};
return best[0];
}
function stagePunctuation(p,a,joined){
const findings=conventionFindings(p,{...a,template:'A'}).filter(x=>['period','capitalization'].includes(x.code));
const required=[];
for(const c of a.clauses)if(c.time&&c.template==='B')required.push({after:c.time.indices.at(-1),before:c.subject?.indices[0]??c.action?.indices[0],phrase:c.time.words.join(' ')});
if(joined){
if(joined.position==='beginning')required.push({after:a.clauses[1].lastIndex,before:a.subject?.indices[0],phrase:'the opening reason'});
else if(['so','and','but'].includes(joined.word))required.push({after:a.lastIndex,before:joined.index,phrase:'the first idea'});
}
const used=new Set(),commas=p.punctuation.filter(x=>x.text===',');
for(const r of required){const comma=commas.find(x=>!used.has(x.index)&&x.index>r.after&&x.index<r.before);if(comma)used.add(comma.index);else findings.push(finding('comma_required','convention',`Add a comma after ${r.phrase}.`,[],[{after:r.after}]));}
const extra=commas.filter(x=>!used.has(x.index));if(extra.length)findings.push(finding('extra_comma','convention','Remove or move the extra comma.',extra.map(x=>x.index)));
return findings;
}
function stageMissingParts(p,c){
// Diagnose omissions only in known single-idea constituents; uncertain order stays unscored.
const words=p.words;if(!words.length||words.some(w=>role(w.lower,'conjunction')))return [];
const opening=time(words,0),start=opening?.end||0,subject=subjects(words,start,true).sort((a,b)=>a.errors.length-b.errors.length)[0],fs=[];
const hasVerb=words.some(w=>role(w.lower,'verb')||role(w.lower,'auxiliary'));
if(subject){
if(!hasVerb&&!subject.errors.some(f=>f.code==='subject_case'))fs.push(finding('missing_action','grammar','Your sentence is missing a verb. Add a verb after the subject.',[],[{after:subject.indices.at(-1)}]));
fs.push(...subject.errors.filter(f=>f.code==='subject_determiner'));
}else{
let at=start;if(DATA.determiners.includes(words[at]?.lower))at++;if(DATA.adjectives.includes(words[at]?.lower))at++;
if(!words.slice(start).some(w=>role(w.lower,'people')||role(w.lower,'subject_pronoun')||role(w.lower,'object_pronoun'))&&action(words,at,true,c.tense))fs.push(finding('missing_subject','grammar','Your sentence is missing a subject noun or pronoun. Add who or what does the action.',[],[{before:words[at].index}]));
}
if(subject){
const before=stageAdverb(words,subject.end),v=action(words,before?.end||subject.end,true,c.tense,subject);
if(v){
const after=before?null:stageAdverb(words,v.end),at=after?.end||v.end;
let head=at;const prep=['at','in','on','to'].includes(words[head]?.lower);if(prep)head++;
const det=DATA.determiners.includes(words[head]?.lower);if(det)head++;if(DATA.adjectives.includes(words[head]?.lower))head++;
if(head>at&&(head===words.length||time(words,head)?.end===words.length)&&(prep||det&&DATA.objectVerbs[v.lemma]))fs.push(finding(prep?'missing_place_noun':'missing_object_noun','grammar',`Add a noun for the ${prep?'place':'object'} after these words.`,[],[{after:words[head-1].index}]));
const place=nounPhrase(words,at,'place',true)[0];
if(place&&(place.end===words.length||time(words,place.end)?.end===words.length)&&place.head!=='home'){
fs.push(finding('missing_preposition','grammar','Add a preposition before the place, such as “at”, “in” or “to”.',[],[{before:place.indices[0]}]));
fs.push(...place.errors.filter(f=>f.code==='place_determiner'));
}
}
}
for(const [key,roleName,label] of [['adjective','adjective','adjective'],['adverb','adverb','adverb']])if(c.framework?.[key]&&!words.some(w=>role(w.lower,roleName)||key==='adverb'&&w.lower===c.framework.adverb))fs.push(finding(`required_${key}`,'task',`Add the required ${label} to ${key==='adjective'?'describe the subject':'tell how the action happened'}.`));
return fs;
}
function evaluateStage(input,c,b){
if(c.scaffold!=null&&typeof c.scaffold!=='boolean')throw {reason:'invalid_challenge'};
const level=DATA.lessons.findIndex(x=>x.id===c.stage),validFrame=f=>f&&typeof f==='object'&&!Array.isArray(f)&&Object.keys(f).every(k=>['subject','number','action','object','location','prep','adjective','adverb','time'].includes(k))&&DATA.nouns.some(x=>x[0]===f.subject)&&(f.number===undefined||['singular','plural'].includes(f.number))&&DATA.verbs.some(x=>x[0]===f.action)&&(!f.location||DATA.places.includes(f.location))&&(!f.prep||f.location&&['at','in','on','to'].includes(f.prep))&&(!f.object||DATA.objectVerbs[f.action]?.includes(f.object))&&(!f.adjective||DATA.adjectives.includes(f.adjective))&&(!f.adverb||DATA.adverbs.some(x=>x[0]===f.adverb))&&(!f.time||DATA.times.some(x=>x.join(' ')===f.time));
if(level<0||level>5||!DATA.chapters.some(x=>x.tense===c.tense)||!['A','B'].includes(c.arrangement||'A')||c.framework&&!validFrame(c.framework)||c.second&&!validFrame(c.second)||c.join&&!role(c.join,'conjunction')||c.joinPosition&&!['beginning','middle'].includes(c.joinPosition))throw {reason:'invalid_challenge'};
if(c.joinPosition==='beginning'&&c.join&&!['because','since'].includes(c.join))throw {reason:'invalid_challenge'};
if(c.source||c.previous||level!==5&&(c.second||c.join||c.joinPosition))throw {reason:'invalid_challenge'};
const p=projection(input,b);if(!p.raw.length)return {outcome:'empty',score:null,marks:{sentence:null,conventions:null},reason:null,findings:[],feedback:'Build a sentence before checking it.',analysis:null,stats:{analyses:b.analyses,diagnostics:b.diagnostics}};
const unsupported=()=>{
const result=uncertain('unsupported_construction',p,b),hints=stageMissingParts(p,c);
return hints.length?{...result,findings:hints,feedback:hints[0].message}:result;
};
const joins=p.words.filter(w=>role(w.lower,'conjunction'));if(joins.length>1)return unsupported();
let main,second,joined,unjoined=false;
if(joins.length){
const j=joins[0],at=p.words.indexOf(j);joined={word:j.lower,index:j.index,position:at===0?'beginning':'middle'};
if(at===0){
const choices=[];
for(let split=2;split<p.words.length;split++){
const reason=stageClause(p.words.slice(1,split),c.tense,b),result=stageClause(p.words.slice(split),c.tense,b);
if(reason?.subject&&reason?.action&&result?.subject)choices.push({main:result,second:reason});
}
if(choices.length!==1)return unsupported();
({main,second}=choices[0]);
}else{main=stageClause(p.words.slice(0,at),c.tense,b);second=stageClause(p.words.slice(at+1),c.tense,b);}
if(!main)return unsupported();
if(!second)second={subject:null,action:null,time:null,grammarErrors:[finding('joining_clause','grammar','Add a complete second idea with a subject and verb.',[])],taskErrors:[],repair:[]};
}else{
main=stageClause(p.words,c.tense,b);
if(!main&&level===5){
// Only diagnose a missing join when exactly two complete clauses resolve uniquely.
const choices=[];
for(let split=2;split<p.words.length-1;split++){
const left=stageClause(p.words.slice(0,split),c.tense,b),right=stageClause(p.words.slice(split),c.tense,b);
if(left?.subject&&left.action?.finite&&right?.subject&&right.action?.finite)choices.push(c.joinPosition==='beginning'?{main:right,second:left}:{main:left,second:right});
}
// The activity attaches time to the main idea; do not mistake it for a second-clause opener.
const cost=x=>x.main.grammarErrors.length+x.second.grammarErrors.length+Number(Boolean(x.second.time));
const minimum=Math.min(...choices.map(cost)),best=choices.filter(x=>cost(x)===minimum);
if(best.length!==1)return unsupported();
({main,second}=best[0]);unjoined=true;
}
}
if(!main)return unsupported();
main.lastIndex??=main.time&&main.template==='A'?main.time.indices.at(-1):(main.location||main.adverb||main.object||main.action||main.subject)?.indices.at(-1);
if(second)second.lastIndex??=second.time&&second.template==='A'?second.time.indices.at(-1):(second.location||second.adverb||second.object||second.action||second.subject)?.indices.at(-1);
const clauses=[main,...(second?[second]:[])],a={...main,clauses},fs=clauses.flatMap(x=>[...x.grammarErrors,...x.taskErrors]);
const task=(code,message,part)=>fs.push(finding(code,'task',message,part?.indices||[]));
if(level===5&&(!joined||!second))fs.unshift(finding('required_join','task',`Join both ideas${c.join?` with “${c.join}”`: ' with a joining word'}${c.joinPosition==='beginning'?' at the beginning':''}.`,unjoined?[second.subject.indices[0]]:[],unjoined?[c.joinPosition==='beginning'?{before:p.words[0].index}:{after:main.lastIndex}]:[]));
const requested=(key,minimum)=>c.framework?Boolean(c.framework[key]):level>=minimum;
if(level===0&&(main.object||main.location))task('stage_extra','Use only a subject and action for this short sentence.',main.object||main.location);
if(level>=1&&!main.object&&!main.location)task('required_detail','Add the object or place for your idea.',main.action);
if(requested('adjective',2)&&!main.subject?.words.some(x=>DATA.adjectives.includes(x)))task('required_adjective','Describe the subject with an adjective.',main.subject);
if(requested('adverb',3)&&!main.adverb)task('required_adverb','Tell how the action happened with an adverb.',main.action);
if(requested('time',4)&&!main.time)task('required_time','Add the required time phrase.',main.action);
if(main.time&&main.template!==(c.arrangement||'A'))task('arrangement',`Put the time at the ${c.arrangement==='B'?'beginning':'end'} of the main idea.`,main.time);
if(level<4&&main.time||level<3&&main.adverb||level<2&&main.subject?.words.some(x=>DATA.adjectives.includes(x)))task('stage_extra','Use only the sentence features requested in this stage.',main.time||main.adverb||main.subject);
if(level<5&&joined)task('stage_extra','Build one idea in this stage.',main.action);
if(level===5){
if(joined&&second){
if(c.join&&joined.word!==c.join)task('join_choice',`Use “${c.join}” to join these ideas.`,{indices:[joined.index]});
if(joined.position!==(c.joinPosition||'middle')||joined.position==='beginning'&&!['because','since'].includes(joined.word))task('join_position',`Put “${c.join||joined.word}” at the ${c.joinPosition==='beginning'?'beginning':'join between the two ideas'}.`,{indices:[joined.index]});
if(second.time)task('stage_extra','Use the time phrase with the main idea only.',second.time);
}
}
for(const [f,clause,label] of [[c.framework,main,'main'],[c.second,second,'second']])if(f){
for(const [key,value] of Object.entries(f)){
if(key==='prep')continue;
if(key==='number'){
if(value&&clause?.subject&&clause.subject.number!==value)task('framework_number',`Keep the subject ${value}: use “${DATA.nouns.find(x=>x[0]===f.subject)[value==='plural'?1:0]}”.`,clause.subject);
continue;
}
const actual=key==='subject'||key==='action'?clause?.[key]?.lemma:key==='object'?clause?.object?.lemma:key==='location'?clause?.location?.head:key==='adjective'?clause?.subject?.words.find(w=>DATA.adjectives.includes(w)):key==='adverb'?clause?.adverb?.lemma:clause?.time?.words.join(' ');
if(value&&actual!==value)task(`framework_${key}`,`Use “${value}” as the ${key} in the ${label} idea.`,clause?.[key]||clause?.subject);
}
}
fs.push(...stagePunctuation(p,a,joined));
const sentence=clauses.every(x=>x.subject&&x.action?.finite)&&!fs.some(x=>x.kind==='grammar'||x.kind==='task'),conventions=!fs.some(x=>x.kind==='convention');
const score=Number(sentence)+Number(conventions);
return {outcome:sentence&&conventions?'pass':'revise',score,marks:{sentence:Number(sentence),conventions:Number(conventions)},reason:null,findings:fs,feedback:fs[0]?.message||'Your sentence follows this stage.',analysis:a,stats:{analyses:b.analyses,diagnostics:b.diagnostics}};
}
function check(input,challenge={arrangement:'A'},overrides) {
let b;
try {b=createBudget(overrides,Boolean(challenge?.stage));return challenge?.stage?evaluateStage(input,challenge,b):evaluate(input,challenge,b);}
catch(e) {if(e && typeof e==='object'&&typeof e.reason==='string')return uncertain(e.reason,null,b);throw e;}
}
// Keep the reviewed pass/diagnosis contract; classroom points measure each skill separately.
function grade(input,challenge={arrangement:'A'}) {
const result=check(input,challenge),a=result.analysis;
const verbCodes=['past_tense','finite_action','missing_action','missing_continuous_action','missing_auxiliary','auxiliary_agreement','continuous_form','tense_choice','future_form'];
const structureCodes=['location_action','extra_component','source_extra'];
const findings=[...result.findings];
if(a&&!challenge.stage&&!findings.some(x=>x.kind==='convention'))findings.push(...conventionFindings(projection(input,createBudget()),a));
const clauses=a?.clauses||[a],clause=clauses.every(x=>x?.subject&&x?.action),core=clause&&clauses.every(x=>x.action.finite),has=test=>findings.some(test);
const marks={
verb:core&&!has(x=>verbCodes.includes(x.code)),
structure:clause&&!has(x=>x.kind==='grammar'&&!verbCodes.includes(x.code)||structureCodes.includes(x.code)||x.code==='required_join'&&clauses.length>1),
task:clause&&!has(x=>x.kind==='task'&&!verbCodes.includes(x.code)&&!structureCodes.includes(x.code)),
capitals:Boolean(a)&&!has(x=>x.code==='capitalization'),
punctuation:Boolean(a)&&!has(x=>x.kind==='convention'&&x.code!=='capitalization')
};
// Supplied roots/order do not earn a task point in the two basic scaffolded stages.
const suppliedTask=challenge.scaffold===true&&['simple','details'].includes(challenge.stage);
if(suppliedTask&&has(x=>x.kind==='task'&&!verbCodes.includes(x.code)))marks.structure=false;
const criteria=Object.entries({verb:'Verb form and agreement',structure:'Sentence structure',task:challenge.scaffold?'Required additions and placement':'Task requirements',capitals:'Capital letters',punctuation:'Punctuation'}).filter(([id])=>!suppliedTask||id!=='task').map(([id,label])=>({id,label,earned:a?Number(marks[id]):null,max:1}));
return {...result,score:a?criteria.reduce((n,x)=>n+x.earned,0):null,maxScore:criteria.length,marks:Object.fromEntries(criteria.map(x=>[x.id,x.earned])),criteria,findings};
}
return Object.freeze({check,grade,tokenize,verbWords});
})();
/* STORE — pure session transitions only. Browser persistence belongs to Stage 4. */
const STORE = (() => {
const clone=x=>JSON.parse(JSON.stringify(x));
function createPair(){return {build:{status:'not_attempted',first:null,last:null},rearrange:{status:'not_attempted',reason:'complete_build_first',first:null,last:null},skippedPair:false};}
function transition(current,event) {
const s=clone(current);
if(event.type==='submit') {
if(!['build','rearrange'].includes(event.task))throw Error('Unknown task');
if(event.result.outcome==='empty')return s;
if(event.task==='rearrange'&&s.build.last?.outcome!=='pass')return s;
const task=s[event.task];
if(!task.first)task.first=clone(event.result);
task.last=clone(event.result);
task.status=event.result.outcome==='pass'&&task.first.outcome!=='pass'?'corrected':event.result.outcome==='unscored'?'submitted_unscored':'submitted_scored';
if(event.task==='build'&&event.result.outcome==='pass'){s.rearrange.reason=null;s.skippedPair=false;}
} else if(event.type==='skip_pair'||event.type==='skip_build') {
if(!s.build.first)s.build.status='skipped';
s.skippedPair=true;
if(s.build.last?.outcome==='pass')s.rearrange.status='skipped';
else{s.rearrange.status='not_attempted';s.rearrange.reason='no_accepted_source';}
} else if(event.type==='skip_rearrange') {
if(s.build.last?.outcome==='pass')s.rearrange.status='skipped';
} else throw Error('Unknown event');
return s;
}
return Object.freeze({createPair,transition});
})();
/* CURRICULUM — finite, authored vocabulary and clause frames.
* ponytail: linked meanings use authored pairs; add more pairs when the pilot needs variety. */
const CURRICULUM = (() => {
const ids=DATA.lessons.slice(0,-1).map(x=>x.id);
// Adverbs describe how an action happens; subject adjectives rotate independently.
const descriptions={
walk:['slow','quick','quiet','calm','gentle','eager'],run:['fast','quick','eager','brave'],
play:['happy','quiet','gentle','eager','calm'],dance:['happy','slow','gentle','eager'],
sing:['loud','quiet','soft','clear','happy'],laugh:['loud','happy','quiet'],
rest:['quiet','calm'],wait:['quiet','patient','calm'],talk:['quiet','clear','polite','calm','soft'],
work:['careful','quiet','neat','patient','eager'],eat:['slow','quiet','eager'],
do:['careful','quiet','neat','patient'],write:['careful','quick','neat','clear'],
carry:['careful','slow','gentle'],clean:['careful','quick','neat'],
paint:['careful','slow','neat'],kick:['quick','careful'],open:['careful','slow','gentle','quiet']
};
// Each complete pool pass visits every entry; the next pass shifts its starting point.
// The period is size squared, not size. A fixed stride would preserve even-pool parity.
const pick=(pool,n)=>pool[(n+Math.floor(n/pool.length))%pool.length];
const joiningIdeas={
reason:[
{"id":"R1","meaning":"Music prompts dancing.","first":{"action":"play","object":"music"},"second":{"action":"dance","location":"classroom","prep":"in"}},
{"id":"R2","meaning":"Opening the door allows someone to walk to the classroom.","first":{"action":"open","object":"door"},"second":{"action":"walk","location":"classroom","prep":"to"}},
{"id":"R3","context":"A game of chase: the {result} must follow the ball toward the playground.","meaning":"Kicking a ball prompts another player to run to the playground.","first":{"action":"kick","object":"ball"},"second":{"action":"run","location":"playground","prep":"to"}},
{"id":"R4","context":"The {result} must get the letter from the {cause} before leaving.","meaning":"One person waits while another writes a letter.","first":{"action":"write","object":"letter"},"second":{"action":"wait","location":"classroom","prep":"in"}}
],
addition:[
{"id":"A1","meaning":"Two ordinary activities: eating and writing.","first":{"action":"eat","object":"lunch"},"second":{"action":"write","object":"letter"}},
{"id":"A2","meaning":"Two ordinary activities: painting and carrying.","first":{"action":"paint","object":"picture"},"second":{"action":"carry","object":"bag"}},
{"id":"A3","meaning":"Two activities during a performance: singing and dancing.","first":{"action":"sing","location":"classroom","prep":"in"},"second":{"action":"dance","location":"classroom","prep":"in"}},
{"id":"A4","meaning":"Two ordinary activities: cleaning a room and opening a door.","first":{"action":"clean","object":"room"},"second":{"action":"open","object":"door"}}
],
contrast:[
{"id":"B1","meaning":"Working contrasts with playing.","first":{"action":"work","location":"classroom","prep":"in"},"second":{"action":"play","location":"park","prep":"in"}},
{"id":"B2","meaning":"Resting at home contrasts with running to the playground.","first":{"action":"rest","location":"home","prep":"at"},"second":{"action":"run","location":"playground","prep":"to"}},
{"id":"B3","meaning":"Waiting in one place contrasts with walking elsewhere.","first":{"action":"wait","location":"school","prep":"at"},"second":{"action":"walk","location":"library","prep":"to"}},
{"id":"B4","meaning":"Talking in the classroom contrasts with resting at home.","first":{"action":"talk","location":"classroom","prep":"in"},"second":{"action":"rest","location":"home","prep":"at"}}
]
};
for(const pairs of Object.values(joiningIdeas)){for(const pair of pairs){Object.freeze(pair.first);Object.freeze(pair.second);Object.freeze(pair);}Object.freeze(pairs);}Object.freeze(joiningIdeas);
for(const choices of Object.values(descriptions))Object.freeze(choices);Object.freeze(descriptions);
function activity(number,tense='simple_past',stage='simple',options={}){
if(!Number.isSafeInteger(number)||number<0)throw Error('Invalid activity number');
const chapter=DATA.chapters.find(c=>c.tense===tense);if(!chapter)throw Error('Unknown tense');
if(!ids.includes(stage))throw Error('Unknown lesson');
for(const key of ['number','secondNumber'])if(options[key]!==undefined&&!['singular','plural'].includes(options[key]))throw Error('Invalid subject number');
const level=ids.indexOf(stage),people=DATA.nouns.map(x=>x[0]),subject=pick(people,number);
const intransitive=DATA.verbs.slice(0,10),transitive=Object.keys(DATA.objectVerbs);
const frame={subject,number:options.number||'singular',action:intransitive[number%intransitive.length][0]};
if(level>0){
if(number%2===0){frame.action=transitive[number%transitive.length];const objects=DATA.objectVerbs[frame.action];frame.object=objects[Math.floor(number/(2*transitive.length))%objects.length];}
else{frame.action=intransitive[Math.floor(number/2)%intransitive.length][0];frame.location=DATA.places[number%DATA.places.length];frame.prep=frame.location==='home'?(['walk','run'].includes(frame.action)?null:'at'):['walk','run'].includes(frame.action)?'to':frame.location==='school'?'at':frame.location==='playground'?'on':'in';}
}
let join,second,joinPosition,ideaId,context,arrangement=options.arrangement||(number%2?'B':'A');
const round=stage==='joining'?(options.join?number:Math.floor(number/5)):Math.floor(number/(frame.object?2*transitive.length:2*intransitive.length));
if(stage==='joining'){
join=options.join||['because','since','so','and','but'][number%5];
const reason=['because','since'].includes(join),kind=['because','since','so'].includes(join)?'reason':join==='and'?'addition':join==='but'?'contrast':null;
if(!kind)throw Error('Unknown joining word');
const pair=pick(joiningIdeas[kind],round);ideaId=pair.id;
delete frame.object;delete frame.location;delete frame.prep;
Object.assign(frame,reason?pair.second:pair.first);
second={subject:pick(people,number+5),number:options.secondNumber||'singular',...(reason?pair.first:pair.second)};
context=(pair.context||pair.meaning).replace('{cause}',subjectWord(reason?second:frame)).replace('{result}',subjectWord(reason?frame:second));
// Rotate allowed profiles on this conjunction's occurrence, independently of global parity.
const profiles=[['middle','A'],['middle','B'],...(reason?[['beginning','A']]:[])].filter(([position,time])=>(!options.joinPosition||position===options.joinPosition)&&(!options.arrangement||time===options.arrangement));
if(!profiles.length)throw Error('Unsupported joining/time profile');
[joinPosition,arrangement]=profiles[round%profiles.length];
}
const adverbs=descriptions[frame.action];
if(level>=2)frame.adjective=pick(DATA.subjectAdjectives,number);
if(level>=3)frame.adverb=adverbs[round%adverbs.length];
if(level>=4)frame.time=pick(chapter.times,number);
const challenge={stage,tense,arrangement,framework:frame};if(second)Object.assign(challenge,{second,join,joinPosition});
const roots=[];
for(const f of [frame,second].filter(Boolean))roots.push(f.subject,f.action,f.object,f.location,f.location?f.prep:null,f.adjective,f.adverb,...(f.time?.split(' ')||[]));
roots.push('the');
for(const f of [frame,second].filter(Boolean)){
const nouns=[...(f.number==='plural'?[]:[f.adjective||f.subject]),...(f.object&&DATA.things.find(x=>x[0]===f.object)?.[1]?[f.object]:[]),...(f.location&&f.prep&&!['home','school'].includes(f.location)?[f.location]:[])];
for(const noun of nouns)roots.push(DATA.lexicon[noun].initialSound==='vowel'?'an':'a');
}
roots.push('be','will');if(join)roots.push(join);
const bank=[...new Set(roots.filter(Boolean))];
return {bank,framework:frame,second,challenge,stage,ideaId,context};
}
function subjectWord(frame){return DATA.nouns.find(x=>x[0]===frame.subject)[frame.number==='plural'?1:0];}
function coreWords(challenge){
const words=f=>[subjectWord(f),f.action,f.object||f.location].filter(Boolean);
if(!challenge.second)return words(challenge.framework);
const frames=challenge.joinPosition==='beginning'?[challenge.second,challenge.framework]:[challenge.framework,challenge.second];
return frames.flatMap(words);
}
function clause(f,tense,arrangement='A'){
const words=['the',...(f.adjective?[f.adjective]:[]),subjectWord(f),...ENGINE.verbWords({lemma:f.subject,number:f.number||'singular'},f.action,tense)];
if(f.object){const countable=!(f.action==='play'&&f.object==='ball')&&DATA.things.find(x=>x[0]===f.object)?.[1];words.push(...(countable?['the']:[]),f.object);}
if(f.adverb&&f.prep!==null)words.push(DATA.adverbs.find(x=>x[0]===f.adverb)[1]);
if(f.location)words.push(...(f.prep?[f.prep]:[]),...(!f.prep||f.location==='home'&&f.prep==='at'||f.location==='school'&&['at','to'].includes(f.prep)?[]:['the']),f.location);
if(f.adverb&&f.prep===null)words.push(DATA.adverbs.find(x=>x[0]===f.adverb)[1]);
if(f.time){if(arrangement==='B')words.unshift(...f.time.split(' '),',');else words.push(...f.time.split(' '));}
return words;
}
function answer(challenge){
let words=clause(challenge.framework,challenge.tense,challenge.arrangement);
if(challenge.second){const other=clause(challenge.second,challenge.tense);words=challenge.joinPosition==='beginning'?[challenge.join,...other,',',...words]:[...words,...(['so','and','but'].includes(challenge.join)?[',']:[]),challenge.join,...other];}
words=words.map(w=>['monday','tuesday','friday','i'].includes(w)?w[0].toUpperCase()+w.slice(1):w);words[0]=words[0][0].toUpperCase()+words[0].slice(1);words.push('.');return words;
}
function example(challenge){
const copy=JSON.parse(JSON.stringify(challenge));copy.framework.subject=challenge.framework.subject==='teacher'?'friend':'teacher';if(copy.second)copy.second.subject=copy.framework.subject==='friend'?'student':'friend';return answer(copy);
}
return Object.freeze({activity,answer,example,coreWords,subjectWord,adverbChoices:descriptions,joiningIdeas});
})();
const AUTHORED = [
{
"id": "C01",
"challenge": {
"arrangement": "A"
},
"tokens": [
"The",
"children",
"walked",
"after",
"school",
"."
],
"expected": {
"outcome": "pass",
"score": 2,
"reason": null,
"findings": [],
"feedback": "Your time phrase comes at the end."
}
},
{
"id": "C02",
"challenge": {
"arrangement": "B"
},
"tokens": [
"After",
"school",
",",
"the",
"children",
"walked",
"."
],
"expected": {
"outcome": "pass",
"score": 2,
"reason": null,
"findings": [],
"feedback": "Your time phrase comes at the beginning."
}
},
{
"id": "C03",
"challenge": {
"arrangement": "B"
},
"tokens": [
"The",
"children",
"walked",
"after",
"school",
"."
],
"expected": {
"outcome": "revise",
"score": 1,
"reason": null,
"findings": [
{
"code": "arrangement",
"kind": "task",
"message": "For this challenge, move “after school” to the beginning.",
"highlights": [
3,
4
],
"anchors": []
}
],
"feedback": "For this challenge, move “after school” to the beginning."
}
},
{
"id": "C04",
"challenge": {
"arrangement": "A"
},
"tokens": [
"After",
"school",
",",
"the",
"children",
"walked",
"."
],
"expected": {
"outcome": "revise",
"score": 1,
"reason": null,
"findings": [
{
"code": "arrangement",
"kind": "task",
"message": "For this challenge, move “after school” to the end.",
"highlights": [
0,
1
],
"anchors": []
}
],
"feedback": "For this challenge, move “after school” to the end."
}
},
{
"id": "C05",
"challenge": {
"arrangement": "B"
},
"tokens": [
"After",
"school",
",",
"the",
"children",
"walk",
"."
],
"expected": {
"outcome": "revise",
"score": 1,
"reason": null,
"findings": [
{
"code": "past_tense",
"kind": "grammar",
"message": "This challenge needs past tense. Change “walk” to “walked.”",
"highlights": [
5
],
"anchors": []
}
],
"feedback": "This challenge needs past tense. Change “walk” to “walked.”"
}
},
{
"id": "C06",
"challenge": {
"arrangement": "B"
},
"tokens": [
"After",
"school",
",",
"the",
"children",
"walking",
"."
],
"expected": {
"outcome": "revise",
"score": 0,
"reason": null,
"findings": [
{
"code": "finite_action",
"kind": "grammar",
"message": "Use a past-tense action, such as “walked.”",
"highlights": [
5
],
"anchors": []
}
],
"feedback": "Use a past-tense action, such as “walked.”"
}
},
{
"id": "C07",
"challenge": {