atris 3.57.3 → 3.58.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -1
- package/atris/skills/clawhub/member-runtime/SKILL.md +9 -9
- package/atris/skills/create-member/SKILL.md +16 -13
- package/atris/skills/design/SKILL.md +27 -4
- package/atris/skills/engines/SKILL.md +57 -13
- package/atris/skills/x-search/SKILL.md +13 -5
- package/atris/skills/youtube/SKILL.md +39 -8
- package/atris/team/brainstormer/MEMBER.md +2 -2
- package/atris/team/executor/MEMBER.md +3 -3
- package/atris/team/launcher/MEMBER.md +4 -4
- package/atris/team/navigator/MEMBER.md +3 -3
- package/atris/team/researcher/MEMBER.md +2 -2
- package/atris/team/validator/MEMBER.md +1 -1
- package/atris.md +14 -0
- package/ax +157 -24
- package/bin/atris.js +79 -31
- package/commands/aeo.js +4 -4
- package/commands/agents.js +1 -1
- package/commands/align.js +12 -12
- package/commands/analytics.js +3 -3
- package/commands/app.js +5 -5
- package/commands/ask.js +175 -0
- package/commands/auth.js +7 -7
- package/commands/autoland.js +29 -5
- package/commands/autopilot-front.js +6 -6
- package/commands/autopilot.js +90 -90
- package/commands/avail.js +5 -5
- package/commands/bench.js +63 -1
- package/commands/browse.js +2 -2
- package/commands/business.js +12 -12
- package/commands/card.js +2 -2
- package/commands/clean.js +13 -13
- package/commands/close.js +297 -15
- package/commands/compile.js +13 -13
- package/commands/computer.js +94 -27
- package/commands/console.js +5 -5
- package/commands/decide.js +1 -1
- package/commands/deck.js +9 -9
- package/commands/drive.js +4 -4
- package/commands/engine.js +173 -19
- package/commands/errors.js +3 -3
- package/commands/experiments.js +18 -1
- package/commands/feed.js +2 -2
- package/commands/feedback.js +4 -4
- package/commands/fleet.js +12 -12
- package/commands/game.js +1 -1
- package/commands/guide.js +49 -0
- package/commands/improve.js +19 -17
- package/commands/init.js +67 -19
- package/commands/interview.js +7 -7
- package/commands/land.js +40 -15
- package/commands/learn.js +12 -12
- package/commands/lesson.js +2 -2
- package/commands/log.js +1 -1
- package/commands/loops.js +4 -4
- package/commands/member.js +75 -50
- package/commands/mission.js +97 -98
- package/commands/one-lap.js +1 -1
- package/commands/pack.js +74 -8
- package/commands/plugin.js +6 -6
- package/commands/probe.js +9 -9
- package/commands/pull.js +35 -35
- package/commands/pulse.js +1 -1
- package/commands/push.js +21 -21
- package/commands/rainmaker.js +1 -1
- package/commands/recap.js +7 -7
- package/commands/reel.js +2 -2
- package/commands/release.js +3 -3
- package/commands/report.js +1 -1
- package/commands/review.js +5 -5
- package/commands/revisions.js +1 -1
- package/commands/run.js +1 -1
- package/commands/scout.js +3 -3
- package/commands/security-review.js +7 -7
- package/commands/serve.js +9 -9
- package/commands/setup.js +1 -1
- package/commands/sign.js +4 -4
- package/commands/signup.js +4 -4
- package/commands/site.js +8 -3
- package/commands/skill.js +6 -6
- package/commands/slop.js +18 -18
- package/commands/social.js +3 -3
- package/commands/soul.js +14 -14
- package/commands/spaceship.js +1 -1
- package/commands/status.js +2 -2
- package/commands/strings.js +13 -13
- package/commands/study.js +4 -4
- package/commands/sync.js +8 -8
- package/commands/task.js +50 -25
- package/commands/teach.js +2 -2
- package/commands/team.js +1 -1
- package/commands/theme.js +2 -2
- package/commands/tree.js +34 -0
- package/commands/truth.js +7 -7
- package/commands/verify.js +12 -12
- package/commands/visualize.js +2 -2
- package/commands/workflow.js +97 -40
- package/commands/worktree.js +57 -1
- package/commands/write.js +9 -9
- package/commands/x-search.js +304 -15
- package/commands/xp.js +6 -3
- package/commands/youtube.js +916 -65
- package/lib/accept-verify-gate.js +3 -3
- package/lib/activity-stream.js +1 -1
- package/lib/apply-gate.js +13 -0
- package/lib/auto-accept-certified.js +28 -12
- package/lib/autoland.js +6 -6
- package/lib/bench/engines.js +59 -8
- package/lib/bench/report.js +1 -1
- package/lib/bench/runner.js +204 -5
- package/lib/bench/tree-render.js +76 -0
- package/lib/card.js +1 -1
- package/lib/chat-log-scan.js +1 -1
- package/lib/claude-boot-block.js +2 -0
- package/lib/deck-compose.js +1 -1
- package/lib/deck-history.js +2 -2
- package/lib/deck-layout.js +1 -1
- package/lib/deck-review.js +5 -5
- package/lib/deck-schema.js +1 -1
- package/lib/default-verifier.js +1 -1
- package/lib/developer-api.js +3 -0
- package/lib/engine-ask.js +66 -9
- package/lib/engine-registry.js +15 -2
- package/lib/file-ops.js +1 -1
- package/lib/fleet.js +71 -35
- package/lib/html-render.js +1 -1
- package/lib/intents.js +172 -0
- package/lib/journal.js +1 -1
- package/lib/known-commands.js +2 -2
- package/lib/land-green.js +364 -0
- package/lib/lesson-contradiction.js +2 -2
- package/lib/lesson-preflight.js +1 -1
- package/lib/manifest.js +5 -5
- package/lib/member-alive.js +9 -5
- package/lib/memory-view.js +3 -3
- package/lib/mission-protected-lane.js +1 -1
- package/lib/mission-root.js +2 -2
- package/lib/orb-context.js +10 -10
- package/lib/pack-capabilities.js +861 -3
- package/lib/permission-grants.js +2 -2
- package/lib/policy-lessons.js +6 -6
- package/lib/pulse.js +8 -8
- package/lib/receipt-evidence.js +1 -1
- package/lib/reel.js +1 -1
- package/lib/review-integrity.js +1 -1
- package/lib/runner-command.js +35 -5
- package/lib/scorecard.js +4 -4
- package/lib/security-scan.js +5 -5
- package/lib/self-drive.js +1 -1
- package/lib/site-publish.js +372 -0
- package/lib/site.js +1 -1
- package/lib/slides-deck.js +2 -2
- package/lib/state-detection.js +2 -2
- package/lib/sync-telemetry.js +2 -2
- package/lib/task-db.js +17 -10
- package/lib/task-explanation.js +1 -1
- package/lib/task-proof.js +2 -2
- package/lib/task-receipt.js +3 -3
- package/lib/theme.js +12 -12
- package/lib/todo-fallback.js +3 -3
- package/lib/todo-sections.js +1 -1
- package/lib/tree-hash.js +79 -0
- package/lib/usage.js +1 -1
- package/lib/wish-delegate.js +1 -1
- package/lib/workspace-safety.js +4 -4
- package/lib/workspace-scaffold.js +3 -3
- package/package.json +1 -1
- package/scripts/det/concierge-bakeoff.sh +26 -0
- package/scripts/det/ytquote-repair.js +0 -1
- package/scripts/det/ytrail-race.js +0 -5
- package/scripts/member-operate.mjs +52 -14
- package/scripts/outbound-artifact-gate.js +0 -3
- package/utils/auth.js +0 -3
- package/utils/config.js +0 -1
- package/scripts/det/checklist-score.js +0 -191
package/commands/youtube.js
CHANGED
|
@@ -30,7 +30,9 @@ function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
|
|
|
30
30
|
output(`Usage: ${commandName} search "<query>" [--limit N] [--json]`);
|
|
31
31
|
output(` ${commandName} search --paid "<query>" [--limit N] [--json]`);
|
|
32
32
|
output(` ${commandName} notes <youtube-url> [youtube-url-or-playlist...] [engine] [--save]`);
|
|
33
|
-
output(` ${commandName} teach <youtube-url> [--section N] [--save]`);
|
|
33
|
+
output(` ${commandName} teach <youtube-url> [--section N] [--save] [--recap TEXT] [--skip]`);
|
|
34
|
+
output(` ${commandName} teach owed [--json]`);
|
|
35
|
+
output(` ${commandName} teach next [--json]`);
|
|
34
36
|
output(` ${commandName} unsave <url-or-id>`);
|
|
35
37
|
output(` ${commandName} process <youtube-url> [options]`);
|
|
36
38
|
output(` ${commandName} digest [--days N]`);
|
|
@@ -40,22 +42,27 @@ function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
|
|
|
40
42
|
output(` ${commandName} watch tick`);
|
|
41
43
|
output(` ${commandName} <youtube-url> [options]`);
|
|
42
44
|
output('');
|
|
43
|
-
output('search = free local discovery (ytsearch / yt-dlp), returns youtu.be links');
|
|
44
|
-
output('search --paid = 5 credits, watch permalinks + titles from Atris');
|
|
45
|
-
output('notes = free local notes to stdout; ephemeral unless --save');
|
|
46
|
-
output('teach = one chapter from local captions;
|
|
47
|
-
output('
|
|
48
|
-
output('
|
|
49
|
-
output('
|
|
45
|
+
output('search = free local discovery (ytsearch / yt-dlp), returns youtu.be links; rich free search prints one failing check; hands off to teach');
|
|
46
|
+
output('search --paid = 5 credits, watch permalinks + titles from Atris; rich paid search prints one failing check; hands off to teach');
|
|
47
|
+
output('notes = free local notes to stdout; ephemeral unless --save; hands off to teach');
|
|
48
|
+
output('teach = one chapter from local captions; bare teach resumes unpaid checks, then the next chapter after recap or skip');
|
|
49
|
+
output('rich ephemeral notes/teach print one apply next-step and one failing check (no files)');
|
|
50
|
+
output('process = 5 credits cloud knowledge (needs a filled Apply); rich process prints one failing check');
|
|
51
|
+
output('digest = one decision page from this week\'s video briefs; rich digest writes one apply and a failing keep/revert pack');
|
|
52
|
+
output('watch = subscribed channels turn into briefs without a human; add hands off to tick; tick hands off to teach when it briefed');
|
|
50
53
|
output('Process a YouTube video through Atris using timestamped transcript-first analysis.');
|
|
51
54
|
output('Falls back to cloud video processing when local captions are unavailable.');
|
|
52
55
|
output('');
|
|
53
56
|
output('Options:');
|
|
54
57
|
output(' --limit <n> Max search results (default: 5)');
|
|
55
58
|
output(' --paid Bill 5 credits for watch permalinks (search only)');
|
|
56
|
-
output(' --save File brief, journal, apply
|
|
59
|
+
output(' --save File brief, journal, apply; rich notes/teach mint a keep/revert experiment');
|
|
57
60
|
output(' --section <n> Chapter to teach, 1-based (teach only, default: 1)');
|
|
58
|
-
output(' --
|
|
61
|
+
output(' --recap <text> Unlock the next teach section with the unpaid check');
|
|
62
|
+
output(' --skip Unlock the next teach section without answering');
|
|
63
|
+
output(' owed Print the unpaid teach check (no network)');
|
|
64
|
+
output(' next Next unlocked chapter (no url, no billing)');
|
|
65
|
+
output(' --unsave Delete filed brief, apply stub, and matching notes/teach experiment packs (no paid calls)');
|
|
59
66
|
output(' --query, -q <text> Focus question for the analysis');
|
|
60
67
|
output(' --agent <id> Agent id to store knowledge against');
|
|
61
68
|
output(' --store Save as agent knowledge (requires --agent)');
|
|
@@ -74,6 +81,7 @@ function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
|
|
|
74
81
|
output(` ${commandName} notes https://www.youtube.com/watch?v=VIDEO_ID --save`);
|
|
75
82
|
output(` ${commandName} teach "https://www.youtube.com/watch?v=VIDEO_ID"`);
|
|
76
83
|
output(` ${commandName} teach "https://www.youtube.com/watch?v=VIDEO_ID" --section 2`);
|
|
84
|
+
output(` ${commandName} teach next`);
|
|
77
85
|
output(` ${commandName} notes --unsave VIDEO_ID`);
|
|
78
86
|
output(` ${commandName} unsave VIDEO_ID`);
|
|
79
87
|
output(` ${commandName} notes https://www.youtube.com/watch?v=VIDEO_ID https://youtu.be/OTHER_ID`);
|
|
@@ -504,6 +512,17 @@ async function processYoutube(options, deps = {}) {
|
|
|
504
512
|
return result.data;
|
|
505
513
|
}
|
|
506
514
|
|
|
515
|
+
function processAnalysisText(data) {
|
|
516
|
+
return data?.video_analysis || data?.analysis || data?.result || '';
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function printProcessLearnerGate(data, { json = false } = {}, output) {
|
|
520
|
+
printLearnerCheckGate(output, notesLessonFromText(processAnalysisText(data)), {
|
|
521
|
+
includeCheck: true,
|
|
522
|
+
json,
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
507
526
|
function formatYoutubeResult(data) {
|
|
508
527
|
const lines = [];
|
|
509
528
|
const metadata = data?.metadata || {};
|
|
@@ -523,7 +542,7 @@ function formatYoutubeResult(data) {
|
|
|
523
542
|
const remaining = data.credits_remaining !== undefined ? data.credits_remaining : '?';
|
|
524
543
|
lines.push(`Credits: ${used} used, ${remaining} remaining`);
|
|
525
544
|
}
|
|
526
|
-
const analysis = data
|
|
545
|
+
const analysis = processAnalysisText(data);
|
|
527
546
|
if (analysis) {
|
|
528
547
|
lines.push('');
|
|
529
548
|
lines.push(String(analysis).trim());
|
|
@@ -564,22 +583,24 @@ function parseNotesArgs(argv = []) {
|
|
|
564
583
|
let help = false;
|
|
565
584
|
let save = false;
|
|
566
585
|
let unsave = false;
|
|
586
|
+
let json = false;
|
|
567
587
|
for (const raw of argv) {
|
|
568
588
|
const arg = String(raw);
|
|
569
589
|
if (arg === '--help' || arg === '-h' || arg === 'help') help = true;
|
|
570
590
|
else if (arg === '--save') save = true;
|
|
571
591
|
else if (arg === '--unsave') unsave = true;
|
|
592
|
+
else if (arg === '--json') json = true;
|
|
572
593
|
}
|
|
573
594
|
for (const raw of argv) {
|
|
574
595
|
const arg = String(raw);
|
|
575
596
|
if (arg === '--help' || arg === '-h' || arg === 'help') continue;
|
|
576
|
-
if (arg === '--save' || arg === '--unsave') continue;
|
|
597
|
+
if (arg === '--save' || arg === '--unsave' || arg === '--json') continue;
|
|
577
598
|
if (arg.startsWith('-')) continue;
|
|
578
599
|
if (looksLikeYoutubeUrl(arg)) urls.push(arg);
|
|
579
600
|
else if (unsave && videoIdFromArg(arg)) urls.push(arg);
|
|
580
601
|
else engine = arg;
|
|
581
602
|
}
|
|
582
|
-
return { urls, engine, help, save, unsave };
|
|
603
|
+
return { urls, engine, help, save, unsave, json };
|
|
583
604
|
}
|
|
584
605
|
|
|
585
606
|
function dateStamp(now) {
|
|
@@ -645,16 +666,69 @@ function applySidecarRel(id) {
|
|
|
645
666
|
return applyGate.applySidecarRel('youtube', id);
|
|
646
667
|
}
|
|
647
668
|
|
|
648
|
-
function
|
|
669
|
+
function notesExperimentSlug(id) {
|
|
670
|
+
return `notes-${experimentIdToken(id)}`;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function notesExperimentRel(id) {
|
|
674
|
+
return `atris/experiments/${notesExperimentSlug(id)}`;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function notesLessonFromText(text) {
|
|
678
|
+
const body = String(text || '');
|
|
679
|
+
return {
|
|
680
|
+
numbers: extractTeachNumbers(body),
|
|
681
|
+
mechanisms: extractTeachMechanisms(body),
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function readNotesText({ url, workDir } = {}) {
|
|
686
|
+
const id = videoIdFromUrl(url);
|
|
687
|
+
if (!id || !workDir) return '';
|
|
688
|
+
const notesPath = path.join(workDir, `yt_${id}.md`);
|
|
689
|
+
try {
|
|
690
|
+
if (!fs.existsSync(notesPath)) return '';
|
|
691
|
+
return fs.readFileSync(notesPath, 'utf8');
|
|
692
|
+
} catch {
|
|
693
|
+
return '';
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function saveRichNotes(url, deps = {}) {
|
|
698
|
+
const workDir = deps.workDir || path.join(process.env.TMPDIR || '/tmp', 'ytnotes');
|
|
699
|
+
const lesson = notesLessonFromText(readNotesText({ url, workDir }));
|
|
700
|
+
if (isThinTeachLesson(lesson)) {
|
|
701
|
+
return { thin: true, brief: null, packRel: null, lesson };
|
|
702
|
+
}
|
|
703
|
+
const brief = fileNotesBrief(url, deps);
|
|
649
704
|
const id = videoIdFromUrl(url);
|
|
705
|
+
const packRel = fileTeachExperiment({
|
|
706
|
+
cwd: deps.cwd || process.cwd(),
|
|
707
|
+
url,
|
|
708
|
+
lesson,
|
|
709
|
+
slug: id ? notesExperimentSlug(id) : null,
|
|
710
|
+
applyRel: id ? applySidecarRel(id) : null,
|
|
711
|
+
});
|
|
712
|
+
return { thin: false, brief, packRel, lesson };
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function ensureNotesApply({ cwd, url, packRel, now, output } = {}) {
|
|
716
|
+
const id = videoIdFromUrl(url);
|
|
717
|
+
const pack = packRel || (id ? notesExperimentRel(id) : null);
|
|
718
|
+
const slug = pack ? path.basename(pack) : null;
|
|
650
719
|
return applyGate.ensureApply({
|
|
651
720
|
cwd,
|
|
652
721
|
source: url,
|
|
653
722
|
rel: id ? applySidecarRel(id) : null,
|
|
654
723
|
now,
|
|
655
724
|
output,
|
|
656
|
-
incompleteMessage:
|
|
725
|
+
incompleteMessage: slug
|
|
726
|
+
? `next: atris experiments keep ${slug}`
|
|
727
|
+
: APPLY_NEXT_MESSAGE,
|
|
657
728
|
required: false,
|
|
729
|
+
change: pack ? `apply ${pack}` : undefined,
|
|
730
|
+
receipt: pack ? TEACH_KEEP_RULE : undefined,
|
|
731
|
+
journalLine: pack ? `- [claimable] apply: ${pack}. ${TEACH_KEEP_RULE}` : undefined,
|
|
658
732
|
});
|
|
659
733
|
}
|
|
660
734
|
|
|
@@ -662,6 +736,50 @@ function youtubeBriefRel(id) {
|
|
|
662
736
|
return `atris/wiki/briefs/youtube-${id}.md`;
|
|
663
737
|
}
|
|
664
738
|
|
|
739
|
+
function removeUnsaveRel(cwd, rel, removed) {
|
|
740
|
+
const abs = path.join(cwd, rel);
|
|
741
|
+
try {
|
|
742
|
+
if (!fs.existsSync(abs)) return;
|
|
743
|
+
const st = fs.lstatSync(abs);
|
|
744
|
+
if (st.isDirectory()) fs.rmSync(abs, { recursive: true, force: true });
|
|
745
|
+
else fs.unlinkSync(abs);
|
|
746
|
+
removed.push(rel);
|
|
747
|
+
} catch {
|
|
748
|
+
// already gone or unreadable: do not error
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function listTeachSectionNumbers(cwd, id) {
|
|
753
|
+
const root = path.join(cwd, 'atris', 'experiments');
|
|
754
|
+
const sections = [];
|
|
755
|
+
try {
|
|
756
|
+
if (!fs.existsSync(root)) return sections;
|
|
757
|
+
for (const name of fs.readdirSync(root)) {
|
|
758
|
+
const match = String(name).match(/^teach-.+-s(\d+)$/);
|
|
759
|
+
if (!match) continue;
|
|
760
|
+
const section = Number(match[1]);
|
|
761
|
+
if (name === teachExperimentSlug(id, section)) sections.push(section);
|
|
762
|
+
}
|
|
763
|
+
} catch {
|
|
764
|
+
// missing experiments dir is fine
|
|
765
|
+
}
|
|
766
|
+
return sections.sort((a, b) => a - b);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function listTeachSidecarRels(cwd, id) {
|
|
770
|
+
const briefsDir = path.join(cwd, 'atris', 'wiki', 'briefs');
|
|
771
|
+
const prefix = `youtube-${id}-s`;
|
|
772
|
+
try {
|
|
773
|
+
if (!fs.existsSync(briefsDir)) return [];
|
|
774
|
+
return fs.readdirSync(briefsDir)
|
|
775
|
+
.filter((name) => name.startsWith(prefix) && name.endsWith('.md'))
|
|
776
|
+
.sort((a, b) => a.localeCompare(b, 'en'))
|
|
777
|
+
.map((name) => `atris/wiki/briefs/${name}`);
|
|
778
|
+
} catch {
|
|
779
|
+
return [];
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
665
783
|
function unsaveYoutubeNotes(target, deps = {}) {
|
|
666
784
|
const output = deps.output || ((line = '') => console.log(line));
|
|
667
785
|
const cwd = deps.cwd || process.cwd();
|
|
@@ -672,18 +790,26 @@ function unsaveYoutubeNotes(target, deps = {}) {
|
|
|
672
790
|
}
|
|
673
791
|
const briefRel = youtubeBriefRel(id);
|
|
674
792
|
const applyRel = applySidecarRel(id);
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
793
|
+
const sections = listTeachSectionNumbers(cwd, id);
|
|
794
|
+
const rels = [];
|
|
795
|
+
const seen = new Set();
|
|
796
|
+
const add = (rel) => {
|
|
797
|
+
if (!rel || seen.has(rel)) return;
|
|
798
|
+
seen.add(rel);
|
|
799
|
+
rels.push(rel);
|
|
800
|
+
};
|
|
801
|
+
add(briefRel);
|
|
802
|
+
add(applyRel);
|
|
803
|
+
for (const section of sections) {
|
|
804
|
+
add(teachBriefRel(id, section));
|
|
805
|
+
add(applySidecarRel(`${id}-s${section}`));
|
|
686
806
|
}
|
|
807
|
+
for (const rel of listTeachSidecarRels(cwd, id)) add(rel);
|
|
808
|
+
add(notesExperimentRel(id));
|
|
809
|
+
for (const section of sections) add(teachExperimentRel(id, section));
|
|
810
|
+
|
|
811
|
+
const removed = [];
|
|
812
|
+
for (const rel of rels) removeUnsaveRel(cwd, rel, removed);
|
|
687
813
|
if (!removed.length) {
|
|
688
814
|
output(`already gone: ${briefRel} and ${applyRel}`);
|
|
689
815
|
return 0;
|
|
@@ -708,6 +834,9 @@ function runYoutubeUnsave(args = [], deps = {}) {
|
|
|
708
834
|
const status = unsaveYoutubeNotes(target, deps);
|
|
709
835
|
if (status !== 0) code = status;
|
|
710
836
|
}
|
|
837
|
+
if (code === 0 && parsed.json !== true && deps.json !== true) {
|
|
838
|
+
printWatchSearchNext(output);
|
|
839
|
+
}
|
|
711
840
|
return code;
|
|
712
841
|
}
|
|
713
842
|
|
|
@@ -870,6 +999,104 @@ function appendDigestJournal({ cwd, date, relDigest }) {
|
|
|
870
999
|
fs.writeFileSync(journalPath, `${existing}${prefix}${line}\n`);
|
|
871
1000
|
}
|
|
872
1001
|
|
|
1002
|
+
function digestExperimentSlug(date) {
|
|
1003
|
+
return `digest-${String(date || '').slice(0, 10)}`;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
function digestExperimentRel(date) {
|
|
1007
|
+
return `atris/experiments/${digestExperimentSlug(date)}`;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function digestApplyRel(date) {
|
|
1011
|
+
return applyGate.applySidecarRel('digest', String(date || '').slice(0, 10));
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function ensureDigestApply({ cwd, date, packRel, now, output, source } = {}) {
|
|
1015
|
+
const stamp = String(date || '').slice(0, 10);
|
|
1016
|
+
const pack = packRel || (stamp ? digestExperimentRel(stamp) : null);
|
|
1017
|
+
const slug = pack ? path.basename(pack) : null;
|
|
1018
|
+
return applyGate.ensureApply({
|
|
1019
|
+
cwd,
|
|
1020
|
+
source: source || (stamp ? `digest:${stamp}` : 'digest'),
|
|
1021
|
+
rel: stamp ? digestApplyRel(stamp) : null,
|
|
1022
|
+
now,
|
|
1023
|
+
output,
|
|
1024
|
+
incompleteMessage: slug
|
|
1025
|
+
? `next: atris experiments keep ${slug}`
|
|
1026
|
+
: applyGate.ephemeralApplyMessage('digest'),
|
|
1027
|
+
required: false,
|
|
1028
|
+
change: pack ? `apply ${pack}` : undefined,
|
|
1029
|
+
receipt: pack ? TEACH_KEEP_RULE : undefined,
|
|
1030
|
+
journalLine: pack ? `- [claimable] apply: ${pack}. ${TEACH_KEEP_RULE}` : undefined,
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
function saveRichDigest({ cwd, date, lesson, source } = {}) {
|
|
1035
|
+
if (isThinTeachLesson(lesson)) {
|
|
1036
|
+
return { thin: true, packRel: null, lesson };
|
|
1037
|
+
}
|
|
1038
|
+
const packRel = fileTeachExperiment({
|
|
1039
|
+
cwd,
|
|
1040
|
+
lesson,
|
|
1041
|
+
slug: date ? digestExperimentSlug(date) : null,
|
|
1042
|
+
applyRel: date ? digestApplyRel(date) : null,
|
|
1043
|
+
});
|
|
1044
|
+
return { thin: false, packRel, lesson, source };
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
function watchExperimentSlug(id) {
|
|
1048
|
+
return `watch-${experimentIdToken(id)}`;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function watchExperimentRel(id) {
|
|
1052
|
+
return `atris/experiments/${watchExperimentSlug(id)}`;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
function watchApplyRel(id) {
|
|
1056
|
+
return applyGate.applySidecarRel('watch', experimentIdToken(id));
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function ensureWatchWiki(cwd) {
|
|
1060
|
+
if (!cwd) return;
|
|
1061
|
+
fs.mkdirSync(path.join(cwd, 'atris', 'wiki'), { recursive: true });
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function ensureWatchApply({ cwd, url, packRel, now, output, source } = {}) {
|
|
1065
|
+
const id = videoIdFromUrl(url);
|
|
1066
|
+
const pack = packRel || (id ? watchExperimentRel(id) : null);
|
|
1067
|
+
const slug = pack ? path.basename(pack) : null;
|
|
1068
|
+
ensureWatchWiki(cwd);
|
|
1069
|
+
return applyGate.ensureApply({
|
|
1070
|
+
cwd,
|
|
1071
|
+
source: source || url || (id ? `watch:${id}` : 'watch'),
|
|
1072
|
+
rel: id ? watchApplyRel(id) : null,
|
|
1073
|
+
now,
|
|
1074
|
+
output,
|
|
1075
|
+
incompleteMessage: slug
|
|
1076
|
+
? `next: atris experiments keep ${slug}`
|
|
1077
|
+
: applyGate.ephemeralApplyMessage('watch'),
|
|
1078
|
+
required: false,
|
|
1079
|
+
change: pack ? `apply ${pack}` : undefined,
|
|
1080
|
+
receipt: pack ? TEACH_KEEP_RULE : undefined,
|
|
1081
|
+
journalLine: pack ? `- [claimable] apply: ${pack}. ${TEACH_KEEP_RULE}` : undefined,
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
function saveRichWatch({ cwd, url, lesson } = {}) {
|
|
1086
|
+
if (isThinTeachLesson(lesson)) {
|
|
1087
|
+
return { thin: true, packRel: null, lesson };
|
|
1088
|
+
}
|
|
1089
|
+
const id = videoIdFromUrl(url);
|
|
1090
|
+
const packRel = fileTeachExperiment({
|
|
1091
|
+
cwd,
|
|
1092
|
+
url,
|
|
1093
|
+
lesson,
|
|
1094
|
+
slug: id ? watchExperimentSlug(id) : null,
|
|
1095
|
+
applyRel: id ? watchApplyRel(id) : null,
|
|
1096
|
+
});
|
|
1097
|
+
return { thin: false, packRel, lesson, source: url };
|
|
1098
|
+
}
|
|
1099
|
+
|
|
873
1100
|
function runYoutubeDigest(args = [], deps = {}) {
|
|
874
1101
|
const output = deps.output || ((line = '') => console.log(line));
|
|
875
1102
|
let options;
|
|
@@ -889,6 +1116,7 @@ function runYoutubeDigest(args = [], deps = {}) {
|
|
|
889
1116
|
const briefs = collectVideoBriefs({ cwd, now, days: options.days });
|
|
890
1117
|
if (!briefs.length) {
|
|
891
1118
|
output(`no video briefs in the last ${options.days} days`);
|
|
1119
|
+
printWatchSearchNext(output);
|
|
892
1120
|
return 0;
|
|
893
1121
|
}
|
|
894
1122
|
|
|
@@ -916,7 +1144,32 @@ function runYoutubeDigest(args = [], deps = {}) {
|
|
|
916
1144
|
fs.writeFileSync(path.join(cwd, relDigest), `${header}\n\n${text}\n`);
|
|
917
1145
|
appendDigestJournal({ cwd, date, relDigest });
|
|
918
1146
|
output(`digest filed: ${relDigest} (${briefs.length} briefs)`);
|
|
919
|
-
|
|
1147
|
+
const lesson = notesLessonFromText(text);
|
|
1148
|
+
if (isThinTeachLesson(lesson)) {
|
|
1149
|
+
printLearnerCheckGate(output, lesson, { includeCheck: true });
|
|
1150
|
+
printWatchTickNext(output);
|
|
1151
|
+
return 0;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
const saved = saveRichDigest({ cwd, date, lesson, source: relDigest });
|
|
1155
|
+
const ensureApply = deps.ensureApply || ensureDigestApply;
|
|
1156
|
+
const applyCode = ensureApply({
|
|
1157
|
+
cwd,
|
|
1158
|
+
date,
|
|
1159
|
+
packRel: saved.packRel,
|
|
1160
|
+
now,
|
|
1161
|
+
output,
|
|
1162
|
+
source: relDigest,
|
|
1163
|
+
});
|
|
1164
|
+
if (deps.ensureApply) return applyCode;
|
|
1165
|
+
const baseline = proveSavedLearnerBaseline({
|
|
1166
|
+
cwd,
|
|
1167
|
+
applyRel: digestApplyRel(date),
|
|
1168
|
+
lesson,
|
|
1169
|
+
output,
|
|
1170
|
+
});
|
|
1171
|
+
if (baseline !== 0) return baseline;
|
|
1172
|
+
return applyCode;
|
|
920
1173
|
}
|
|
921
1174
|
|
|
922
1175
|
function watchStatePath(cwd = process.cwd()) {
|
|
@@ -1065,6 +1318,22 @@ function markSeen(state, channel, id, timestamp) {
|
|
|
1065
1318
|
state.seenByChannel[channel][id] = timestamp;
|
|
1066
1319
|
}
|
|
1067
1320
|
|
|
1321
|
+
const WATCH_TICK_NEXT = 'next: atris youtube watch tick';
|
|
1322
|
+
const WATCH_ADD_NEXT = 'next: atris youtube watch add <channel-url-or-@handle>';
|
|
1323
|
+
const WATCH_SEARCH_NEXT = 'next: atris youtube search " "';
|
|
1324
|
+
|
|
1325
|
+
function printWatchTickNext(output) {
|
|
1326
|
+
output(WATCH_TICK_NEXT);
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
function printWatchAddNext(output) {
|
|
1330
|
+
output(WATCH_ADD_NEXT);
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
function printWatchSearchNext(output) {
|
|
1334
|
+
output(WATCH_SEARCH_NEXT);
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1068
1337
|
function addWatchChannel(channelInput, deps = {}) {
|
|
1069
1338
|
const output = deps.output || ((line = '') => console.log(line));
|
|
1070
1339
|
if (!channelInput) {
|
|
@@ -1084,6 +1353,7 @@ function addWatchChannel(channelInput, deps = {}) {
|
|
|
1084
1353
|
const state = loadWatchState(statePath);
|
|
1085
1354
|
if (state.channels.some((row) => row.channel === channel)) {
|
|
1086
1355
|
output(`already watching ${channel}`);
|
|
1356
|
+
printWatchTickNext(output);
|
|
1087
1357
|
return 0;
|
|
1088
1358
|
}
|
|
1089
1359
|
|
|
@@ -1093,6 +1363,7 @@ function addWatchChannel(channelInput, deps = {}) {
|
|
|
1093
1363
|
});
|
|
1094
1364
|
saveWatchState(statePath, state);
|
|
1095
1365
|
output(`watching ${channel}`);
|
|
1366
|
+
printWatchTickNext(output);
|
|
1096
1367
|
return 0;
|
|
1097
1368
|
}
|
|
1098
1369
|
|
|
@@ -1101,6 +1372,7 @@ function listWatchChannels(deps = {}) {
|
|
|
1101
1372
|
const state = loadWatchState(resolveWatchStatePath(deps));
|
|
1102
1373
|
if (!state.channels.length) {
|
|
1103
1374
|
output('no channels watched');
|
|
1375
|
+
printWatchAddNext(output);
|
|
1104
1376
|
return 0;
|
|
1105
1377
|
}
|
|
1106
1378
|
|
|
@@ -1108,6 +1380,7 @@ function listWatchChannels(deps = {}) {
|
|
|
1108
1380
|
const count = Object.keys(channelSeenMap(state, row.channel)).length;
|
|
1109
1381
|
output(`${index + 1}. ${row.channel} (${count} seen)`);
|
|
1110
1382
|
});
|
|
1383
|
+
printWatchTickNext(output);
|
|
1111
1384
|
return 0;
|
|
1112
1385
|
}
|
|
1113
1386
|
|
|
@@ -1124,6 +1397,11 @@ function removeWatchChannel(rawNumber, deps = {}) {
|
|
|
1124
1397
|
const [removed] = state.channels.splice(index - 1, 1);
|
|
1125
1398
|
saveWatchState(statePath, state);
|
|
1126
1399
|
output(`removed ${removed.channel}`);
|
|
1400
|
+
if (!state.channels.length) {
|
|
1401
|
+
printWatchAddNext(output);
|
|
1402
|
+
} else {
|
|
1403
|
+
printWatchTickNext(output);
|
|
1404
|
+
}
|
|
1127
1405
|
return 0;
|
|
1128
1406
|
}
|
|
1129
1407
|
|
|
@@ -1141,6 +1419,7 @@ async function tickWatch(deps = {}) {
|
|
|
1141
1419
|
const state = loadWatchState(statePath);
|
|
1142
1420
|
let totalNew = 0;
|
|
1143
1421
|
let totalBriefed = 0;
|
|
1422
|
+
let firstBriefedUrl = null;
|
|
1144
1423
|
|
|
1145
1424
|
for (const row of state.channels) {
|
|
1146
1425
|
const videosUrl = channelVideosUrl(row.channel);
|
|
@@ -1187,6 +1466,7 @@ async function tickWatch(deps = {}) {
|
|
|
1187
1466
|
}
|
|
1188
1467
|
markSeen(state, row.channel, video.id, timestamp);
|
|
1189
1468
|
briefed += 1;
|
|
1469
|
+
if (!firstBriefedUrl) firstBriefedUrl = url;
|
|
1190
1470
|
}
|
|
1191
1471
|
|
|
1192
1472
|
if (!isFresh) {
|
|
@@ -1204,6 +1484,40 @@ async function tickWatch(deps = {}) {
|
|
|
1204
1484
|
|
|
1205
1485
|
output(`total: ${totalNew} new, ${totalBriefed} briefed`);
|
|
1206
1486
|
saveWatchState(statePath, state);
|
|
1487
|
+
if (totalBriefed > 0) {
|
|
1488
|
+
if (firstBriefedUrl) {
|
|
1489
|
+
const lesson = notesLessonFromText(readNotesText({ url: firstBriefedUrl, workDir }));
|
|
1490
|
+
if (isThinTeachLesson(lesson)) {
|
|
1491
|
+
printLearnerCheckGate(output, lesson, { includeCheck: true });
|
|
1492
|
+
printYoutubeTeachNext(firstBriefedUrl, {}, output);
|
|
1493
|
+
return 0;
|
|
1494
|
+
}
|
|
1495
|
+
const saved = saveRichWatch({ cwd, url: firstBriefedUrl, lesson });
|
|
1496
|
+
const ensureApply = deps.ensureApply || ensureWatchApply;
|
|
1497
|
+
const applyCode = ensureApply({
|
|
1498
|
+
cwd,
|
|
1499
|
+
url: firstBriefedUrl,
|
|
1500
|
+
packRel: saved.packRel,
|
|
1501
|
+
now,
|
|
1502
|
+
output,
|
|
1503
|
+
source: firstBriefedUrl,
|
|
1504
|
+
});
|
|
1505
|
+
if (deps.ensureApply) return applyCode;
|
|
1506
|
+
const id = videoIdFromUrl(firstBriefedUrl);
|
|
1507
|
+
const baseline = proveSavedLearnerBaseline({
|
|
1508
|
+
cwd,
|
|
1509
|
+
applyRel: id ? watchApplyRel(id) : null,
|
|
1510
|
+
lesson,
|
|
1511
|
+
output,
|
|
1512
|
+
});
|
|
1513
|
+
if (baseline !== 0) return baseline;
|
|
1514
|
+
return applyCode;
|
|
1515
|
+
}
|
|
1516
|
+
printYoutubeTeachNext(firstBriefedUrl, {}, output);
|
|
1517
|
+
return 0;
|
|
1518
|
+
}
|
|
1519
|
+
if (!state.channels.length) printWatchAddNext(output);
|
|
1520
|
+
else printWatchSearchNext(output);
|
|
1207
1521
|
return 0;
|
|
1208
1522
|
}
|
|
1209
1523
|
|
|
@@ -1339,11 +1653,34 @@ function runOneNotesItem(item, engine, deps = {}) {
|
|
|
1339
1653
|
} catch {
|
|
1340
1654
|
status = 1;
|
|
1341
1655
|
}
|
|
1342
|
-
|
|
1656
|
+
let brief = null;
|
|
1657
|
+
let lesson = null;
|
|
1658
|
+
let ok = status === 0;
|
|
1659
|
+
if (ok && deps.save) {
|
|
1660
|
+
const saved = saveRichNotes(item.url, deps);
|
|
1661
|
+
if (saved.thin) {
|
|
1662
|
+
output(TEACH_THIN_REFUSE);
|
|
1663
|
+
ok = false;
|
|
1664
|
+
} else {
|
|
1665
|
+
brief = saved.brief;
|
|
1666
|
+
lesson = saved.lesson;
|
|
1667
|
+
const ensureApply = deps.ensureApply || ensureNotesApply;
|
|
1668
|
+
try {
|
|
1669
|
+
ensureApply({
|
|
1670
|
+
cwd: deps.cwd || process.cwd(),
|
|
1671
|
+
url: item.url,
|
|
1672
|
+
packRel: saved.packRel,
|
|
1673
|
+
now: deps.now,
|
|
1674
|
+
output,
|
|
1675
|
+
});
|
|
1676
|
+
} catch {
|
|
1677
|
+
// apply filing must never break the batch
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1343
1681
|
const seconds = Math.max(0, Math.round((readNowMs(deps) - started) / 1000));
|
|
1344
|
-
const ok = status === 0;
|
|
1345
1682
|
output(`${label} ${seconds}s ${ok ? (brief || 'ok') : 'FAILED'}`);
|
|
1346
|
-
return { url: item.url, id: item.id, seconds, ok, brief };
|
|
1683
|
+
return { url: item.url, id: item.id, seconds, ok, brief, lesson };
|
|
1347
1684
|
}
|
|
1348
1685
|
|
|
1349
1686
|
function formatNotesSummary(rows = []) {
|
|
@@ -1355,7 +1692,14 @@ function formatNotesSummary(rows = []) {
|
|
|
1355
1692
|
return lines.join('\n');
|
|
1356
1693
|
}
|
|
1357
1694
|
|
|
1358
|
-
function
|
|
1695
|
+
function printEphemeralNotesLearnerGate(url, { json = false, workDir } = {}, output) {
|
|
1696
|
+
const dir = workDir || path.join(process.env.TMPDIR || '/tmp', 'ytnotes');
|
|
1697
|
+
const lesson = notesLessonFromText(readNotesText({ url, workDir: dir }));
|
|
1698
|
+
if (!isThinTeachLesson(lesson)) applyGate.hintEphemeralApply(output, 'notes');
|
|
1699
|
+
printLearnerCheckGate(output, lesson, { includeCheck: true, json });
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
function runYoutubeNotesBatch({ urls, engine, save, json } = {}, deps = {}) {
|
|
1359
1703
|
deps = { ...deps, save: save === true || deps.save === true };
|
|
1360
1704
|
const output = deps.output || ((line = '') => console.error(line));
|
|
1361
1705
|
const items = expandNotesTargets(urls || [], deps);
|
|
@@ -1368,7 +1712,30 @@ function runYoutubeNotesBatch({ urls, engine, save } = {}, deps = {}) {
|
|
|
1368
1712
|
output(formatNotesSummary(rows));
|
|
1369
1713
|
}
|
|
1370
1714
|
if (!rows.length) return 2;
|
|
1371
|
-
|
|
1715
|
+
const firstOk = rows.find((row) => row.ok);
|
|
1716
|
+
const asJson = json === true || deps.json === true;
|
|
1717
|
+
if (firstOk && !deps.save) {
|
|
1718
|
+
if (!asJson) printEphemeralNotesLearnerGate(firstOk.url, { workDir: deps.workDir }, output);
|
|
1719
|
+
printYoutubeTeachNext(firstOk.url, { json: asJson }, output);
|
|
1720
|
+
}
|
|
1721
|
+
if (firstOk && deps.save) {
|
|
1722
|
+
if (deps.ensureApply) return 0;
|
|
1723
|
+
const id = firstOk.id || videoIdFromUrl(firstOk.url);
|
|
1724
|
+
const workDir = deps.workDir || path.join(process.env.TMPDIR || '/tmp', 'ytnotes');
|
|
1725
|
+
const lesson = firstOk.lesson || notesLessonFromText(readNotesText({
|
|
1726
|
+
url: firstOk.url,
|
|
1727
|
+
workDir,
|
|
1728
|
+
}));
|
|
1729
|
+
const baseline = proveSavedLearnerBaseline({
|
|
1730
|
+
cwd: deps.cwd || process.cwd(),
|
|
1731
|
+
applyRel: id ? applySidecarRel(id) : null,
|
|
1732
|
+
lesson,
|
|
1733
|
+
output,
|
|
1734
|
+
json: asJson,
|
|
1735
|
+
});
|
|
1736
|
+
if (baseline !== 0) return baseline;
|
|
1737
|
+
}
|
|
1738
|
+
return firstOk ? 0 : 2;
|
|
1372
1739
|
}
|
|
1373
1740
|
|
|
1374
1741
|
function runSingleYoutubeNotes(url, engine, deps = {}) {
|
|
@@ -1380,15 +1747,39 @@ function runSingleYoutubeNotes(url, engine, deps = {}) {
|
|
|
1380
1747
|
}
|
|
1381
1748
|
const status = readRunnerStatus(result);
|
|
1382
1749
|
if (status !== 0) return status;
|
|
1383
|
-
|
|
1384
|
-
|
|
1750
|
+
const output = deps.output || ((line = '') => console.error(line));
|
|
1751
|
+
if (!deps.save) {
|
|
1752
|
+
const json = deps.json === true;
|
|
1753
|
+
printEphemeralNotesLearnerGate(url, { json, workDir: deps.workDir }, output);
|
|
1754
|
+
printYoutubeTeachNext(url, { json }, output);
|
|
1755
|
+
return 0;
|
|
1756
|
+
}
|
|
1757
|
+
const saved = saveRichNotes(url, deps);
|
|
1758
|
+
if (saved.thin) {
|
|
1759
|
+
output(TEACH_THIN_REFUSE);
|
|
1760
|
+
printYoutubeTeachNext(url, { json: deps.json }, output);
|
|
1761
|
+
return 2;
|
|
1762
|
+
}
|
|
1385
1763
|
const ensureApply = deps.ensureApply || ensureNotesApply;
|
|
1386
|
-
|
|
1387
|
-
|
|
1764
|
+
const cwd = deps.cwd || process.cwd();
|
|
1765
|
+
const applyCode = ensureApply({
|
|
1766
|
+
cwd,
|
|
1388
1767
|
url,
|
|
1768
|
+
packRel: saved.packRel,
|
|
1389
1769
|
now: deps.now,
|
|
1390
1770
|
output: deps.output,
|
|
1391
1771
|
});
|
|
1772
|
+
if (deps.ensureApply) return applyCode;
|
|
1773
|
+
const id = videoIdFromUrl(url);
|
|
1774
|
+
const baseline = proveSavedLearnerBaseline({
|
|
1775
|
+
cwd,
|
|
1776
|
+
applyRel: id ? applySidecarRel(id) : null,
|
|
1777
|
+
lesson: saved.lesson,
|
|
1778
|
+
output,
|
|
1779
|
+
json: deps.json === true,
|
|
1780
|
+
});
|
|
1781
|
+
if (baseline !== 0) return baseline;
|
|
1782
|
+
return applyCode;
|
|
1392
1783
|
}
|
|
1393
1784
|
|
|
1394
1785
|
function runYoutubeNotes(args = [], deps = {}) {
|
|
@@ -1406,7 +1797,7 @@ function runYoutubeNotes(args = [], deps = {}) {
|
|
|
1406
1797
|
output(YTNOTES_HINT);
|
|
1407
1798
|
return 2;
|
|
1408
1799
|
}
|
|
1409
|
-
const nextDeps = { ...deps, save: parsed.save };
|
|
1800
|
+
const nextDeps = { ...deps, save: parsed.save, json: parsed.json };
|
|
1410
1801
|
if (parsed.urls.length === 1 && !isPlaylistUrl(parsed.urls[0])) {
|
|
1411
1802
|
return runSingleYoutubeNotes(parsed.urls[0], parsed.engine, nextDeps);
|
|
1412
1803
|
}
|
|
@@ -1436,10 +1827,13 @@ function showYoutubeSearchHelp(output = console.log, commandName = 'atris youtub
|
|
|
1436
1827
|
output('');
|
|
1437
1828
|
output('Free local discovery. Uses ytsearch on PATH when present, else the');
|
|
1438
1829
|
output('bundled scripts/det/ytsearch, else yt-dlp ytsearchN with the same print contract.');
|
|
1439
|
-
output('Does not bill credits.
|
|
1830
|
+
output('Does not bill credits. A hit prints one next: atris youtube teach <first-url>.');
|
|
1831
|
+
output('A rich hit prints one failing check (score 0). A thin hit prints check: fill this.');
|
|
1440
1832
|
output('');
|
|
1441
1833
|
output(`--paid buys watch permalinks from Atris (${PAID_SEARCH_COST_HINT}).`);
|
|
1442
1834
|
output('Requires login. Same auth path as atris youtube process.');
|
|
1835
|
+
output('A hit also prints one next: atris youtube teach <first-url>.');
|
|
1836
|
+
output('A rich hit prints one failing check (score 0). A thin hit prints check: fill this.');
|
|
1443
1837
|
output('Empty or failed paid search refunds the credits.');
|
|
1444
1838
|
output('');
|
|
1445
1839
|
output('Options:');
|
|
@@ -1570,6 +1964,17 @@ function resolveSearchCachePath(deps = {}) {
|
|
|
1570
1964
|
return path.join(homeDir, '.atris', LOCAL_SEARCH_CACHE_FILE);
|
|
1571
1965
|
}
|
|
1572
1966
|
|
|
1967
|
+
function printYoutubeTeachNext(url, options, output) {
|
|
1968
|
+
if (options && options.json) return;
|
|
1969
|
+
if (!url) return;
|
|
1970
|
+
output(`next: atris youtube teach ${quoteYoutubeUrl(url)}`);
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
function printSearchTeachNext(rows, options, output) {
|
|
1974
|
+
const firstUrl = Array.isArray(rows) && rows[0] && rows[0].url;
|
|
1975
|
+
printYoutubeTeachNext(firstUrl, options, output);
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1573
1978
|
function printSearchRows(rows, options, output) {
|
|
1574
1979
|
if (options.json) {
|
|
1575
1980
|
output(JSON.stringify(rows, null, 2));
|
|
@@ -1769,12 +2174,29 @@ async function runPaidYoutubeSearch(options, deps = {}) {
|
|
|
1769
2174
|
const rendered = formatPaidSearchResults(data);
|
|
1770
2175
|
if (!videos.length) {
|
|
1771
2176
|
output(rendered ? `no videos found\n${rendered}` : 'no videos found');
|
|
2177
|
+
printWatchTickNext(output);
|
|
1772
2178
|
return 2;
|
|
1773
2179
|
}
|
|
1774
2180
|
output(rendered);
|
|
2181
|
+
printSearchLearnerGate(videos, options, output);
|
|
2182
|
+
printSearchTeachNext(videos, options, output);
|
|
1775
2183
|
return 0;
|
|
1776
2184
|
}
|
|
1777
2185
|
|
|
2186
|
+
function searchLessonText(rows) {
|
|
2187
|
+
return (Array.isArray(rows) ? rows : [])
|
|
2188
|
+
.map((row) => String(row && row.title ? row.title : '').trim())
|
|
2189
|
+
.filter(Boolean)
|
|
2190
|
+
.join('\n');
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
function printSearchLearnerGate(rows, options, output) {
|
|
2194
|
+
printLearnerCheckGate(output, notesLessonFromText(searchLessonText(rows)), {
|
|
2195
|
+
includeCheck: true,
|
|
2196
|
+
json: Boolean(options && options.json),
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
|
|
1778
2200
|
function commandOnPath(name, deps = {}) {
|
|
1779
2201
|
const spawn = deps.spawnSync || spawnSync;
|
|
1780
2202
|
const result = spawn('sh', ['-c', `command -v ${shellSingleQuote(name)}`], {
|
|
@@ -1910,6 +2332,7 @@ async function runYoutubeSearch(args = [], deps = {}) {
|
|
|
1910
2332
|
if (cached) {
|
|
1911
2333
|
const rows = cached.rows.slice(0, options.limit);
|
|
1912
2334
|
printSearchRows(rows, options, output);
|
|
2335
|
+
printSearchTeachNext(rows, options, output);
|
|
1913
2336
|
output(LOCAL_SEARCH_CACHE_NOTE);
|
|
1914
2337
|
return 0;
|
|
1915
2338
|
}
|
|
@@ -1928,17 +2351,26 @@ async function runYoutubeSearch(args = [], deps = {}) {
|
|
|
1928
2351
|
const rows = parseSearchStdout(stdout);
|
|
1929
2352
|
if (!rows.length) {
|
|
1930
2353
|
output('no videos found');
|
|
2354
|
+
if (!options.json) printWatchTickNext(output);
|
|
1931
2355
|
return 2;
|
|
1932
2356
|
}
|
|
1933
2357
|
|
|
1934
2358
|
writeLocalSearchCache(options.query, rows, deps);
|
|
1935
2359
|
printSearchRows(rows, options, output);
|
|
2360
|
+
printSearchLearnerGate(rows, options, output);
|
|
2361
|
+
printSearchTeachNext(rows, options, output);
|
|
1936
2362
|
return 0;
|
|
1937
2363
|
}
|
|
1938
2364
|
|
|
1939
|
-
const YTTEACH_USAGE = 'usage: atris youtube teach <youtube-url> [--section N] [--save]';
|
|
2365
|
+
const YTTEACH_USAGE = 'usage: atris youtube teach <youtube-url> [--section N] [--save] [--recap TEXT] [--skip] | owed | next';
|
|
1940
2366
|
const TEACH_PAID_REFUSE = 'teach is free local captions. drop --paid.';
|
|
1941
2367
|
const TEACH_THIN_REFUSE = 'thin: no number or named mechanism. no brief.';
|
|
2368
|
+
const TEACH_OWED_FILE = 'youtube-teach-owed.json';
|
|
2369
|
+
const TEACH_RECAP_MISSING = '--recap needs the unpaid check';
|
|
2370
|
+
const TEACH_RESUME_START = 'atris youtube teach <url>';
|
|
2371
|
+
const TEACH_RESUME_NEXT = 'next: atris youtube teach recap TEXT or atris youtube teach skip';
|
|
2372
|
+
const TEACH_WATCH_TICK_NEXT = WATCH_TICK_NEXT;
|
|
2373
|
+
const TEACH_CONTINUE_NEXT = 'next: atris youtube teach next';
|
|
1942
2374
|
const TEACH_APPLY_NEXT_MESSAGE = APPLY_NEXT_MESSAGE;
|
|
1943
2375
|
const TEACH_KEEP_RULE = 'keep only if measure.py moves 0→1. scores 1 only when the fixture contains the check tokens.';
|
|
1944
2376
|
const MECHANISM_STOP = new Set([
|
|
@@ -1965,11 +2397,18 @@ function parseTeachArgs(argv = []) {
|
|
|
1965
2397
|
const options = {
|
|
1966
2398
|
help: false,
|
|
1967
2399
|
save: false,
|
|
2400
|
+
json: false,
|
|
2401
|
+
skip: false,
|
|
2402
|
+
owed: false,
|
|
2403
|
+
resume: false,
|
|
2404
|
+
next: false,
|
|
2405
|
+
recap: null,
|
|
1968
2406
|
url: null,
|
|
1969
2407
|
section: 1,
|
|
1970
2408
|
};
|
|
2409
|
+
let sectionSet = false;
|
|
1971
2410
|
|
|
1972
|
-
if (args.length
|
|
2411
|
+
if (args.length > 0 && ['help', '--help', '-h'].includes(args[0])) {
|
|
1973
2412
|
options.help = true;
|
|
1974
2413
|
return options;
|
|
1975
2414
|
}
|
|
@@ -1980,8 +2419,25 @@ function parseTeachArgs(argv = []) {
|
|
|
1980
2419
|
options.help = true;
|
|
1981
2420
|
} else if (arg === '--save') {
|
|
1982
2421
|
options.save = true;
|
|
2422
|
+
} else if (arg === '--json') {
|
|
2423
|
+
options.json = true;
|
|
2424
|
+
} else if (arg === '--skip') {
|
|
2425
|
+
options.skip = true;
|
|
2426
|
+
} else if (arg === '--owed') {
|
|
2427
|
+
options.owed = true;
|
|
1983
2428
|
} else if (arg === '--paid') {
|
|
1984
2429
|
throw new Error(TEACH_PAID_REFUSE);
|
|
2430
|
+
} else if (arg === '--recap') {
|
|
2431
|
+
const raw = args[i + 1];
|
|
2432
|
+
if (raw == null || String(raw).startsWith('--')) {
|
|
2433
|
+
throw new Error(TEACH_RECAP_MISSING);
|
|
2434
|
+
}
|
|
2435
|
+
options.recap = String(raw);
|
|
2436
|
+
i += 1;
|
|
2437
|
+
} else if (arg.startsWith('--recap=')) {
|
|
2438
|
+
const value = arg.slice('--recap='.length);
|
|
2439
|
+
if (!value) throw new Error(TEACH_RECAP_MISSING);
|
|
2440
|
+
options.recap = value;
|
|
1985
2441
|
} else if (arg === '--section') {
|
|
1986
2442
|
const raw = args[i + 1];
|
|
1987
2443
|
const value = Number.parseInt(raw, 10);
|
|
@@ -1989,6 +2445,7 @@ function parseTeachArgs(argv = []) {
|
|
|
1989
2445
|
throw new Error('--section must be a positive integer');
|
|
1990
2446
|
}
|
|
1991
2447
|
options.section = value;
|
|
2448
|
+
sectionSet = true;
|
|
1992
2449
|
i += 1;
|
|
1993
2450
|
} else if (arg.startsWith('--section=')) {
|
|
1994
2451
|
const value = Number.parseInt(arg.slice('--section='.length), 10);
|
|
@@ -1996,8 +2453,25 @@ function parseTeachArgs(argv = []) {
|
|
|
1996
2453
|
throw new Error('--section must be a positive integer');
|
|
1997
2454
|
}
|
|
1998
2455
|
options.section = value;
|
|
2456
|
+
sectionSet = true;
|
|
1999
2457
|
} else if (arg.startsWith('-')) {
|
|
2000
2458
|
throw new Error(`Unknown option: ${arg}`);
|
|
2459
|
+
} else if (arg === 'recap' && options.recap == null && !options.url) {
|
|
2460
|
+
const parts = [];
|
|
2461
|
+
i += 1;
|
|
2462
|
+
while (i < args.length && !String(args[i]).startsWith('--')) {
|
|
2463
|
+
parts.push(String(args[i]));
|
|
2464
|
+
i += 1;
|
|
2465
|
+
}
|
|
2466
|
+
i -= 1;
|
|
2467
|
+
options.recap = parts.join(' ').trim();
|
|
2468
|
+
if (!options.recap) throw new Error(TEACH_RECAP_MISSING);
|
|
2469
|
+
} else if (arg === 'skip' && !options.url) {
|
|
2470
|
+
options.skip = true;
|
|
2471
|
+
} else if (arg === 'owed' && !options.url) {
|
|
2472
|
+
options.owed = true;
|
|
2473
|
+
} else if (arg === 'next' && !options.url) {
|
|
2474
|
+
options.next = true;
|
|
2001
2475
|
} else if (!options.url && looksLikeYoutubeUrl(arg)) {
|
|
2002
2476
|
options.url = arg;
|
|
2003
2477
|
} else {
|
|
@@ -2006,7 +2480,14 @@ function parseTeachArgs(argv = []) {
|
|
|
2006
2480
|
}
|
|
2007
2481
|
|
|
2008
2482
|
if (options.help) return options;
|
|
2009
|
-
if (!options.url
|
|
2483
|
+
if (!options.url && options.recap == null && !options.skip && !options.owed && !options.next) {
|
|
2484
|
+
if (!sectionSet && !options.save) {
|
|
2485
|
+
options.owed = true;
|
|
2486
|
+
options.resume = true;
|
|
2487
|
+
return options;
|
|
2488
|
+
}
|
|
2489
|
+
throw new Error('Missing YouTube URL. Run "atris youtube teach --help".');
|
|
2490
|
+
}
|
|
2010
2491
|
return options;
|
|
2011
2492
|
}
|
|
2012
2493
|
|
|
@@ -2124,7 +2605,13 @@ function extractTeachMechanisms(text) {
|
|
|
2124
2605
|
return found.slice(0, 8);
|
|
2125
2606
|
}
|
|
2126
2607
|
|
|
2127
|
-
|
|
2608
|
+
const LEARNER_CHECK_FILL = 'fill this';
|
|
2609
|
+
const LEARNER_SCORE_ZERO = 'score: 0';
|
|
2610
|
+
const LEARNER_CHECK_INCOMPLETE = 'incomplete: no measurable check. check: fill this';
|
|
2611
|
+
const LEARNER_BASELINE_INCOMPLETE = 'incomplete: check already passes. refuse invented success.';
|
|
2612
|
+
const LEARNER_APPLY_MISSING = 'incomplete: apply missing. check cannot score.';
|
|
2613
|
+
|
|
2614
|
+
function oneTeachCheck(mechanisms, numbers) {
|
|
2128
2615
|
if (mechanisms[0]) {
|
|
2129
2616
|
const name = mechanisms[0];
|
|
2130
2617
|
const named = new RegExp(`\\b(?:${MECHANISM_HEADS})\\b`, 'i').test(name);
|
|
@@ -2132,7 +2619,63 @@ function oneTeachCheck(mechanisms, numbers, title) {
|
|
|
2132
2619
|
return `what is ${name}?`;
|
|
2133
2620
|
}
|
|
2134
2621
|
if (numbers[0]) return `what does ${numbers[0]} measure in this chapter?`;
|
|
2135
|
-
return
|
|
2622
|
+
return LEARNER_CHECK_FILL;
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
function learnerCheckFromLesson(lesson = {}) {
|
|
2626
|
+
const mechanisms = Array.isArray(lesson.mechanisms) ? lesson.mechanisms : [];
|
|
2627
|
+
const numbers = Array.isArray(lesson.numbers) ? lesson.numbers : [];
|
|
2628
|
+
const inferred = mechanisms.length > 0 || numbers.length > 0;
|
|
2629
|
+
return {
|
|
2630
|
+
inferred,
|
|
2631
|
+
line: inferred ? oneTeachCheck(mechanisms, numbers) : LEARNER_CHECK_FILL,
|
|
2632
|
+
needles: teachCheckNeedles({ mechanisms, numbers }),
|
|
2633
|
+
};
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
function scoreLearnerNeedles(text, needles) {
|
|
2637
|
+
const list = Array.isArray(needles) ? needles : [];
|
|
2638
|
+
if (!list.length) return 0;
|
|
2639
|
+
const blob = String(text || '').toLowerCase();
|
|
2640
|
+
return list.every((needle) => blob.includes(String(needle || '').toLowerCase())) ? 1 : 0;
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
function printLearnerCheckGate(output, lesson, { includeCheck = false, json = false } = {}) {
|
|
2644
|
+
if (json || typeof output !== 'function') return;
|
|
2645
|
+
const check = learnerCheckFromLesson(lesson);
|
|
2646
|
+
if (includeCheck) output(`check: ${check.line}`);
|
|
2647
|
+
if (check.inferred) output(LEARNER_SCORE_ZERO);
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
function proveSavedLearnerBaseline({ cwd, applyRel, lesson, output, json } = {}) {
|
|
2651
|
+
const print = typeof output === 'function' ? output : () => {};
|
|
2652
|
+
const check = learnerCheckFromLesson(lesson);
|
|
2653
|
+
if (!check.inferred) {
|
|
2654
|
+
if (!json) print(LEARNER_CHECK_INCOMPLETE);
|
|
2655
|
+
return 2;
|
|
2656
|
+
}
|
|
2657
|
+
if (!cwd || !applyRel) {
|
|
2658
|
+
if (!json) print(LEARNER_APPLY_MISSING);
|
|
2659
|
+
return 2;
|
|
2660
|
+
}
|
|
2661
|
+
const abs = path.join(cwd, applyRel);
|
|
2662
|
+
let text = '';
|
|
2663
|
+
try {
|
|
2664
|
+
if (!fs.existsSync(abs)) {
|
|
2665
|
+
if (!json) print(LEARNER_APPLY_MISSING);
|
|
2666
|
+
return 2;
|
|
2667
|
+
}
|
|
2668
|
+
text = fs.readFileSync(abs, 'utf8');
|
|
2669
|
+
} catch {
|
|
2670
|
+
if (!json) print(LEARNER_APPLY_MISSING);
|
|
2671
|
+
return 2;
|
|
2672
|
+
}
|
|
2673
|
+
if (scoreLearnerNeedles(text, check.needles) !== 0) {
|
|
2674
|
+
if (!json) print(LEARNER_BASELINE_INCOMPLETE);
|
|
2675
|
+
return 2;
|
|
2676
|
+
}
|
|
2677
|
+
if (!json) print(LEARNER_SCORE_ZERO);
|
|
2678
|
+
return 0;
|
|
2136
2679
|
}
|
|
2137
2680
|
|
|
2138
2681
|
function quoteYoutubeUrl(url) {
|
|
@@ -2163,10 +2706,7 @@ function teachLessonFromCues({ url, section, chapters, chapter, cues, title } =
|
|
|
2163
2706
|
lines.push(oneTeachCheck(mechanisms, numbers, heading));
|
|
2164
2707
|
if (section < total) {
|
|
2165
2708
|
lines.push('');
|
|
2166
|
-
lines.push(
|
|
2167
|
-
} else {
|
|
2168
|
-
lines.push('');
|
|
2169
|
-
lines.push('next: last section');
|
|
2709
|
+
lines.push(TEACH_RESUME_NEXT);
|
|
2170
2710
|
}
|
|
2171
2711
|
return { text: lines.join('\n'), numbers, mechanisms };
|
|
2172
2712
|
}
|
|
@@ -2185,14 +2725,17 @@ function teachBriefRel(id, section) {
|
|
|
2185
2725
|
return `atris/wiki/briefs/youtube-${id}-s${section}.md`;
|
|
2186
2726
|
}
|
|
2187
2727
|
|
|
2188
|
-
function
|
|
2189
|
-
|
|
2728
|
+
function experimentIdToken(id) {
|
|
2729
|
+
return String(id || 'video')
|
|
2190
2730
|
.toLowerCase()
|
|
2191
2731
|
.replace(/_/g, '-')
|
|
2192
2732
|
.replace(/[^a-z0-9-]+/g, '')
|
|
2193
2733
|
.replace(/-+/g, '-')
|
|
2194
2734
|
.replace(/^-+|-+$/g, '') || 'video';
|
|
2195
|
-
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
function teachExperimentSlug(id, section) {
|
|
2738
|
+
return `teach-${experimentIdToken(id)}-s${Number(section) || 1}`;
|
|
2196
2739
|
}
|
|
2197
2740
|
|
|
2198
2741
|
function teachExperimentRel(id, section) {
|
|
@@ -2207,22 +2750,256 @@ function teachCheckNeedles(lesson = {}) {
|
|
|
2207
2750
|
return [];
|
|
2208
2751
|
}
|
|
2209
2752
|
|
|
2753
|
+
function teachRecapTokens(lesson = {}) {
|
|
2754
|
+
const tokens = [];
|
|
2755
|
+
const seen = new Set();
|
|
2756
|
+
for (const value of [...(lesson.mechanisms || []), ...(lesson.numbers || [])]) {
|
|
2757
|
+
const token = String(value || '').trim().toLowerCase();
|
|
2758
|
+
if (!token || seen.has(token)) continue;
|
|
2759
|
+
seen.add(token);
|
|
2760
|
+
tokens.push(token);
|
|
2761
|
+
}
|
|
2762
|
+
return tokens;
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
function recapHitsTokens(text, tokens) {
|
|
2766
|
+
const hay = String(text || '').toLowerCase();
|
|
2767
|
+
return (tokens || []).some((token) => {
|
|
2768
|
+
const needle = String(token || '').trim().toLowerCase();
|
|
2769
|
+
return needle.length > 0 && hay.includes(needle);
|
|
2770
|
+
});
|
|
2771
|
+
}
|
|
2772
|
+
|
|
2773
|
+
function teachOwedPath(deps = {}) {
|
|
2774
|
+
if (deps.teachOwedPath) return deps.teachOwedPath;
|
|
2775
|
+
const cwd = deps.cwd || process.cwd();
|
|
2776
|
+
return path.join(cwd, '.atris', TEACH_OWED_FILE);
|
|
2777
|
+
}
|
|
2778
|
+
|
|
2779
|
+
function readTeachOwedStore(deps = {}) {
|
|
2780
|
+
try {
|
|
2781
|
+
const parsed = JSON.parse(fs.readFileSync(teachOwedPath(deps), 'utf8'));
|
|
2782
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {};
|
|
2783
|
+
return parsed;
|
|
2784
|
+
} catch {
|
|
2785
|
+
return {};
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
|
|
2789
|
+
function writeTeachOwedStore(deps, store) {
|
|
2790
|
+
try {
|
|
2791
|
+
const filePath = teachOwedPath(deps);
|
|
2792
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
2793
|
+
fs.writeFileSync(filePath, `${JSON.stringify(store || {})}\n`);
|
|
2794
|
+
} catch {
|
|
2795
|
+
// owed recap is ephemeral; a failed write must not break teach
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
function rememberTeachOwed(deps, { url, section, lesson, total } = {}) {
|
|
2800
|
+
const id = videoIdFromUrl(url);
|
|
2801
|
+
if (!id) return;
|
|
2802
|
+
const store = readTeachOwedStore(deps);
|
|
2803
|
+
const row = {
|
|
2804
|
+
section: Number(section) || 1,
|
|
2805
|
+
check: teachCheckLine(lesson),
|
|
2806
|
+
tokens: teachRecapTokens(lesson),
|
|
2807
|
+
url: url || undefined,
|
|
2808
|
+
};
|
|
2809
|
+
const totalChapters = Number(total);
|
|
2810
|
+
if (Number.isInteger(totalChapters) && totalChapters > 0) row.total = totalChapters;
|
|
2811
|
+
store[id] = row;
|
|
2812
|
+
writeTeachOwedStore(deps, store);
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
function writeTeachContinue(store, id, entry) {
|
|
2816
|
+
const nextSection = (Number(entry && entry.section) || 1) + 1;
|
|
2817
|
+
const total = Number(entry && entry.total);
|
|
2818
|
+
if (Number.isInteger(total) && total > 0 && nextSection > total) {
|
|
2819
|
+
delete store[id];
|
|
2820
|
+
return null;
|
|
2821
|
+
}
|
|
2822
|
+
const cursor = {
|
|
2823
|
+
section: nextSection,
|
|
2824
|
+
url: (entry && entry.url) || undefined,
|
|
2825
|
+
};
|
|
2826
|
+
if (Number.isInteger(total) && total > 0) cursor.total = total;
|
|
2827
|
+
store[id] = cursor;
|
|
2828
|
+
return cursor;
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2831
|
+
function lastTeachContinueEntry(store) {
|
|
2832
|
+
const ids = Object.keys(store || {});
|
|
2833
|
+
for (let i = ids.length - 1; i >= 0; i -= 1) {
|
|
2834
|
+
const entry = store[ids[i]];
|
|
2835
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
2836
|
+
if (String(entry.check || '').trim()) continue;
|
|
2837
|
+
const url = String(entry.url || '').trim();
|
|
2838
|
+
if (!url) continue;
|
|
2839
|
+
return {
|
|
2840
|
+
id: ids[i],
|
|
2841
|
+
url,
|
|
2842
|
+
section: Number(entry.section) || 1,
|
|
2843
|
+
};
|
|
2844
|
+
}
|
|
2845
|
+
return null;
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
function lastTeachUnpaidEntry(store) {
|
|
2849
|
+
const ids = Object.keys(store || {});
|
|
2850
|
+
for (let i = ids.length - 1; i >= 0; i -= 1) {
|
|
2851
|
+
const entry = store[ids[i]];
|
|
2852
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
2853
|
+
const check = String(entry.check || '').trim();
|
|
2854
|
+
if (!check) continue;
|
|
2855
|
+
return {
|
|
2856
|
+
id: ids[i],
|
|
2857
|
+
check,
|
|
2858
|
+
url: entry.url || null,
|
|
2859
|
+
section: Number(entry.section) || 1,
|
|
2860
|
+
};
|
|
2861
|
+
}
|
|
2862
|
+
return null;
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
function previousTeachUnlocked(owed, section) {
|
|
2866
|
+
if (!owed) return true;
|
|
2867
|
+
return Number(owed.section) >= Number(section);
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
function printTeachUnlockNext(parsed, entry, output) {
|
|
2871
|
+
if (!parsed || parsed.json) return;
|
|
2872
|
+
const url = parsed.url || (entry && entry.url) || '';
|
|
2873
|
+
if (!url) return;
|
|
2874
|
+
output(TEACH_CONTINUE_NEXT);
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
function printTeachWatchTickNext(parsed, lesson, total, output) {
|
|
2878
|
+
if (!parsed || parsed.json) return;
|
|
2879
|
+
if (parsed.save && !isThinTeachLesson(lesson)) return;
|
|
2880
|
+
const section = Number(parsed.section) || 0;
|
|
2881
|
+
if (section < Number(total)) return;
|
|
2882
|
+
printWatchTickNext(output);
|
|
2883
|
+
}
|
|
2884
|
+
|
|
2885
|
+
function listTeachOwedEntries(store) {
|
|
2886
|
+
return Object.keys(store || {}).map((id) => {
|
|
2887
|
+
const entry = store[id] && typeof store[id] === 'object' ? store[id] : {};
|
|
2888
|
+
return {
|
|
2889
|
+
id,
|
|
2890
|
+
url: entry.url || null,
|
|
2891
|
+
section: Number(entry.section) || 1,
|
|
2892
|
+
check: String(entry.check || '').trim(),
|
|
2893
|
+
};
|
|
2894
|
+
}).filter((row) => row.check);
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
function printTeachOwed(parsed, deps, output) {
|
|
2898
|
+
const rows = listTeachOwedEntries(readTeachOwedStore(deps));
|
|
2899
|
+
if (parsed.json) {
|
|
2900
|
+
output(JSON.stringify(rows));
|
|
2901
|
+
return 0;
|
|
2902
|
+
}
|
|
2903
|
+
if (!rows.length) {
|
|
2904
|
+
output('nothing owed');
|
|
2905
|
+
if (parsed.resume) output(TEACH_RESUME_START);
|
|
2906
|
+
return 0;
|
|
2907
|
+
}
|
|
2908
|
+
rows.forEach((row, index) => {
|
|
2909
|
+
if (index) output('');
|
|
2910
|
+
output(row.url || row.id);
|
|
2911
|
+
output(`section ${row.section}`);
|
|
2912
|
+
if (row.check) output(row.check);
|
|
2913
|
+
});
|
|
2914
|
+
if (parsed.resume) output(TEACH_RESUME_NEXT);
|
|
2915
|
+
return 0;
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2918
|
+
function applyTeachRecap(parsed, deps, output) {
|
|
2919
|
+
const store = readTeachOwedStore(deps);
|
|
2920
|
+
let id = parsed.url ? videoIdFromUrl(parsed.url) : null;
|
|
2921
|
+
let entry = id ? store[id] : null;
|
|
2922
|
+
if (!entry && !id) {
|
|
2923
|
+
const ids = Object.keys(store);
|
|
2924
|
+
const unpaidIds = ids.filter((key) => store[key] && String(store[key].check || '').trim());
|
|
2925
|
+
if (parsed.recap) {
|
|
2926
|
+
id = unpaidIds.find((key) => recapHitsTokens(parsed.recap, store[key] && store[key].tokens));
|
|
2927
|
+
}
|
|
2928
|
+
if (!id && unpaidIds.length === 1) id = unpaidIds[0];
|
|
2929
|
+
if (!id && ids.length === 1) id = ids[0];
|
|
2930
|
+
entry = id ? store[id] : null;
|
|
2931
|
+
}
|
|
2932
|
+
if (!entry) {
|
|
2933
|
+
if (parsed.skip) return 0;
|
|
2934
|
+
if (!parsed.json) output('recap the previous section first');
|
|
2935
|
+
return 2;
|
|
2936
|
+
}
|
|
2937
|
+
if (!String(entry.check || '').trim()) {
|
|
2938
|
+
printTeachUnlockNext(parsed, entry, output);
|
|
2939
|
+
return 0;
|
|
2940
|
+
}
|
|
2941
|
+
if (parsed.skip) {
|
|
2942
|
+
printTeachUnlockNext(parsed, entry, output);
|
|
2943
|
+
writeTeachContinue(store, id, entry);
|
|
2944
|
+
writeTeachOwedStore(deps, store);
|
|
2945
|
+
return 0;
|
|
2946
|
+
}
|
|
2947
|
+
if (!recapHitsTokens(parsed.recap, entry.tokens)) {
|
|
2948
|
+
if (!parsed.json) output(entry.check);
|
|
2949
|
+
return 2;
|
|
2950
|
+
}
|
|
2951
|
+
printTeachUnlockNext(parsed, entry, output);
|
|
2952
|
+
writeTeachContinue(store, id, entry);
|
|
2953
|
+
writeTeachOwedStore(deps, store);
|
|
2954
|
+
return 0;
|
|
2955
|
+
}
|
|
2956
|
+
|
|
2957
|
+
function applyTeachNext(parsed, deps, output) {
|
|
2958
|
+
const store = readTeachOwedStore(deps);
|
|
2959
|
+
const cont = lastTeachContinueEntry(store);
|
|
2960
|
+
if (cont) {
|
|
2961
|
+
parsed.url = cont.url;
|
|
2962
|
+
parsed.section = cont.section;
|
|
2963
|
+
return 0;
|
|
2964
|
+
}
|
|
2965
|
+
const unpaid = lastTeachUnpaidEntry(store);
|
|
2966
|
+
if (unpaid) {
|
|
2967
|
+
if (!parsed.json) output(unpaid.check);
|
|
2968
|
+
return 2;
|
|
2969
|
+
}
|
|
2970
|
+
if (parsed.json) {
|
|
2971
|
+
output('[]');
|
|
2972
|
+
return 0;
|
|
2973
|
+
}
|
|
2974
|
+
output('nothing owed');
|
|
2975
|
+
output(TEACH_RESUME_START);
|
|
2976
|
+
return 0;
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
function applyTeachResume(parsed, deps, output) {
|
|
2980
|
+
const store = readTeachOwedStore(deps);
|
|
2981
|
+
if (lastTeachUnpaidEntry(store)) return printTeachOwed(parsed, deps, output);
|
|
2982
|
+
return applyTeachNext(parsed, deps, output);
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2210
2985
|
function teachCheckLine(lesson = {}) {
|
|
2211
2986
|
return oneTeachCheck(lesson.mechanisms || [], lesson.numbers || [], '');
|
|
2212
2987
|
}
|
|
2213
2988
|
|
|
2214
|
-
function fileTeachExperiment({ cwd, url, section, lesson } = {}) {
|
|
2989
|
+
function fileTeachExperiment({ cwd, url, section, lesson, slug, applyRel } = {}) {
|
|
2215
2990
|
try {
|
|
2216
|
-
const id = videoIdFromUrl(url);
|
|
2217
|
-
if (!
|
|
2218
|
-
const
|
|
2219
|
-
|
|
2991
|
+
const id = url ? videoIdFromUrl(url) : null;
|
|
2992
|
+
if (!cwd) return null;
|
|
2993
|
+
const packSlug = slug || (id ? teachExperimentSlug(id, section) : null);
|
|
2994
|
+
if (!packSlug) return null;
|
|
2995
|
+
const rel = `atris/experiments/${packSlug}`;
|
|
2220
2996
|
const dir = path.join(cwd, rel);
|
|
2221
2997
|
fs.mkdirSync(dir, { recursive: true });
|
|
2222
2998
|
|
|
2223
2999
|
const check = teachCheckLine(lesson);
|
|
2224
3000
|
const needles = teachCheckNeedles(lesson);
|
|
2225
|
-
const
|
|
3001
|
+
const sidecarRel = applyRel || (id ? applySidecarRel(`${id}-s${section}`) : null);
|
|
3002
|
+
if (!sidecarRel) return null;
|
|
2226
3003
|
const program = [
|
|
2227
3004
|
'# Program',
|
|
2228
3005
|
'',
|
|
@@ -2243,7 +3020,7 @@ function fileTeachExperiment({ cwd, url, section, lesson } = {}) {
|
|
|
2243
3020
|
'EXPERIMENT_DIR = Path(__file__).resolve().parent',
|
|
2244
3021
|
`CHECK = ${JSON.stringify(check)}`,
|
|
2245
3022
|
`NEEDLES = ${JSON.stringify(needles)}`,
|
|
2246
|
-
`DEFAULT_TARGET = ${JSON.stringify(
|
|
3023
|
+
`DEFAULT_TARGET = ${JSON.stringify(sidecarRel)}`,
|
|
2247
3024
|
'',
|
|
2248
3025
|
'',
|
|
2249
3026
|
'def repo_root() -> Path:',
|
|
@@ -2493,14 +3270,15 @@ function fileTeachBrief({ cwd, url, section, lesson, now } = {}) {
|
|
|
2493
3270
|
function ensureTeachApply({ cwd, url, section, packRel, now, output } = {}) {
|
|
2494
3271
|
const id = videoIdFromUrl(url);
|
|
2495
3272
|
const pack = packRel || (id ? teachExperimentRel(id, section) : null);
|
|
3273
|
+
const slug = pack ? path.basename(pack) : null;
|
|
2496
3274
|
return applyGate.ensureApply({
|
|
2497
3275
|
cwd,
|
|
2498
3276
|
source: url,
|
|
2499
3277
|
rel: id ? applySidecarRel(`${id}-s${section}`) : null,
|
|
2500
3278
|
now,
|
|
2501
3279
|
output,
|
|
2502
|
-
incompleteMessage:
|
|
2503
|
-
? `next:
|
|
3280
|
+
incompleteMessage: slug
|
|
3281
|
+
? `next: atris experiments keep ${slug}`
|
|
2504
3282
|
: TEACH_APPLY_NEXT_MESSAGE,
|
|
2505
3283
|
required: false,
|
|
2506
3284
|
change: pack ? `apply ${pack}` : undefined,
|
|
@@ -2559,6 +3337,37 @@ async function runYoutubeTeach(args = [], deps = {}) {
|
|
|
2559
3337
|
return 0;
|
|
2560
3338
|
}
|
|
2561
3339
|
|
|
3340
|
+
const owedDeps = { ...deps, cwd: deps.cwd || process.cwd() };
|
|
3341
|
+
if (parsed.owed) {
|
|
3342
|
+
if (!parsed.resume) return printTeachOwed(parsed, owedDeps, output);
|
|
3343
|
+
const resumeCode = applyTeachResume(parsed, owedDeps, output);
|
|
3344
|
+
if (resumeCode !== 0) return resumeCode;
|
|
3345
|
+
if (!parsed.url) return 0;
|
|
3346
|
+
}
|
|
3347
|
+
if (parsed.recap != null || parsed.skip) {
|
|
3348
|
+
const recapCode = applyTeachRecap(parsed, owedDeps, output);
|
|
3349
|
+
if (recapCode !== 0) return recapCode;
|
|
3350
|
+
if (!parsed.url || parsed.section <= 1) return 0;
|
|
3351
|
+
}
|
|
3352
|
+
if (parsed.next) {
|
|
3353
|
+
const nextCode = applyTeachNext(parsed, owedDeps, output);
|
|
3354
|
+
if (nextCode !== 0) return nextCode;
|
|
3355
|
+
if (!parsed.url) return 0;
|
|
3356
|
+
}
|
|
3357
|
+
|
|
3358
|
+
if (!parsed.url) {
|
|
3359
|
+
output('Missing YouTube URL. Run "atris youtube teach --help".');
|
|
3360
|
+
return 2;
|
|
3361
|
+
}
|
|
3362
|
+
|
|
3363
|
+
if (parsed.section > 1) {
|
|
3364
|
+
const owed = readTeachOwedStore(owedDeps)[videoIdFromUrl(parsed.url) || ''];
|
|
3365
|
+
if (!previousTeachUnlocked(owed, parsed.section)) {
|
|
3366
|
+
if (!parsed.json && owed && owed.check) output(owed.check);
|
|
3367
|
+
return 2;
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
|
|
2562
3371
|
const source = await (deps.extractTeachSource || extractTeachSource)(parsed.url, deps);
|
|
2563
3372
|
if (!source || !Array.isArray(source.cues) || !source.cues.length) {
|
|
2564
3373
|
output('no english captions for this url. teach stays local and will not call process.');
|
|
@@ -2582,35 +3391,59 @@ async function runYoutubeTeach(args = [], deps = {}) {
|
|
|
2582
3391
|
title: source.title,
|
|
2583
3392
|
});
|
|
2584
3393
|
output(lesson.text);
|
|
3394
|
+
rememberTeachOwed(owedDeps, {
|
|
3395
|
+
url: parsed.url,
|
|
3396
|
+
section: parsed.section,
|
|
3397
|
+
lesson,
|
|
3398
|
+
total: chapters.length,
|
|
3399
|
+
});
|
|
2585
3400
|
|
|
2586
|
-
if (!parsed.save)
|
|
3401
|
+
if (!parsed.save) {
|
|
3402
|
+
if (!isThinTeachLesson(lesson)) applyGate.hintEphemeralApply(output, 'teach');
|
|
3403
|
+
printLearnerCheckGate(output, lesson, { includeCheck: true, json: parsed.json === true });
|
|
3404
|
+
printTeachWatchTickNext(parsed, lesson, chapters.length, output);
|
|
3405
|
+
return 0;
|
|
3406
|
+
}
|
|
2587
3407
|
if (isThinTeachLesson(lesson)) {
|
|
2588
3408
|
output(TEACH_THIN_REFUSE);
|
|
3409
|
+
printTeachWatchTickNext(parsed, lesson, chapters.length, output);
|
|
2589
3410
|
return 2;
|
|
2590
3411
|
}
|
|
2591
3412
|
|
|
3413
|
+
const cwd = deps.cwd || process.cwd();
|
|
2592
3414
|
fileTeachBrief({
|
|
2593
|
-
cwd
|
|
3415
|
+
cwd,
|
|
2594
3416
|
url: parsed.url,
|
|
2595
3417
|
section: parsed.section,
|
|
2596
3418
|
lesson: lesson.text,
|
|
2597
3419
|
now: deps.now,
|
|
2598
3420
|
});
|
|
2599
3421
|
const packRel = fileTeachExperiment({
|
|
2600
|
-
cwd
|
|
3422
|
+
cwd,
|
|
2601
3423
|
url: parsed.url,
|
|
2602
3424
|
section: parsed.section,
|
|
2603
3425
|
lesson,
|
|
2604
3426
|
});
|
|
2605
3427
|
const ensureApply = deps.ensureApply || ensureTeachApply;
|
|
2606
|
-
|
|
2607
|
-
cwd
|
|
3428
|
+
const applyCode = ensureApply({
|
|
3429
|
+
cwd,
|
|
2608
3430
|
url: parsed.url,
|
|
2609
3431
|
section: parsed.section,
|
|
2610
3432
|
packRel,
|
|
2611
3433
|
now: deps.now,
|
|
2612
3434
|
output,
|
|
2613
3435
|
});
|
|
3436
|
+
if (deps.ensureApply) return applyCode;
|
|
3437
|
+
const id = videoIdFromUrl(parsed.url);
|
|
3438
|
+
const baseline = proveSavedLearnerBaseline({
|
|
3439
|
+
cwd,
|
|
3440
|
+
applyRel: id ? applySidecarRel(`${id}-s${parsed.section}`) : null,
|
|
3441
|
+
lesson,
|
|
3442
|
+
output,
|
|
3443
|
+
json: parsed.json === true,
|
|
3444
|
+
});
|
|
3445
|
+
if (baseline !== 0) return baseline;
|
|
3446
|
+
return applyCode;
|
|
2614
3447
|
}
|
|
2615
3448
|
|
|
2616
3449
|
async function youtubeCommand(argv = process.argv.slice(3), deps = {}) {
|
|
@@ -2655,7 +3488,12 @@ async function youtubeCommand(argv = process.argv.slice(3), deps = {}) {
|
|
|
2655
3488
|
let status = 0;
|
|
2656
3489
|
try {
|
|
2657
3490
|
const data = await processYoutube(options, deps);
|
|
2658
|
-
|
|
3491
|
+
if (options.json) {
|
|
3492
|
+
output(JSON.stringify(data, null, 2));
|
|
3493
|
+
} else {
|
|
3494
|
+
output(formatYoutubeResult(data));
|
|
3495
|
+
printProcessLearnerGate(data, {}, output);
|
|
3496
|
+
}
|
|
2659
3497
|
} catch (err) {
|
|
2660
3498
|
if (!err.applyRequired) output(err.message);
|
|
2661
3499
|
status = Number.isInteger(err.exitCode) ? err.exitCode : 1;
|
|
@@ -2703,8 +3541,21 @@ module.exports = {
|
|
|
2703
3541
|
extractTeachNumbers,
|
|
2704
3542
|
extractTeachMechanisms,
|
|
2705
3543
|
extractTeachSource,
|
|
3544
|
+
oneTeachCheck,
|
|
3545
|
+
learnerCheckFromLesson,
|
|
3546
|
+
scoreLearnerNeedles,
|
|
3547
|
+
LEARNER_CHECK_FILL,
|
|
3548
|
+
LEARNER_SCORE_ZERO,
|
|
3549
|
+
printLearnerCheckGate,
|
|
3550
|
+
proveSavedLearnerBaseline,
|
|
2706
3551
|
isThinTeachLesson,
|
|
2707
3552
|
TEACH_THIN_REFUSE,
|
|
3553
|
+
TEACH_RESUME_NEXT,
|
|
3554
|
+
TEACH_WATCH_TICK_NEXT,
|
|
2708
3555
|
teachExperimentSlug,
|
|
3556
|
+
notesExperimentSlug,
|
|
3557
|
+
digestExperimentSlug,
|
|
3558
|
+
watchExperimentSlug,
|
|
3559
|
+
fileTeachExperiment,
|
|
2709
3560
|
youtubeCommand,
|
|
2710
3561
|
};
|