@salesforce/afv-skills 1.45.0 → 1.47.0
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/package.json +1 -1
- package/skills/agentforce-observe/SKILL.md +32 -4
- package/skills/agentforce-observe/references/ahm-alerts.md +719 -0
- package/skills/automation-flow-generate/SKILL.md +11 -5
- package/skills/consumer-goods-promotion-bo-api-deploy/SKILL.md +275 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/README.md +32 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/SetCommentValue.cls +75 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/SetCommentValue.cls-meta.xml +5 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/interview-answers.json +13 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/payloads/copy.json +10 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/payloads/create.json +20 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/payloads/update.json +16 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/references/conventions-and-payload-rules.md +273 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/references/generate-and-wire.md +236 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/references/reference-example-set-comment-value.md +132 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/references/smoke-and-verify.md +211 -0
- package/skills/dx-code-analyzer-configure/scripts/validate-config.sh +14 -10
- package/skills/dx-code-analyzer-run/scripts/apply-fixes.js +45 -4
- package/skills/dx-code-analyzer-run/scripts/describe-rule.js +52 -32
- package/skills/dx-devops-project-manage/SKILL.md +197 -0
- package/skills/dx-devops-project-manage/examples/common-workflows.md +197 -0
- package/skills/dx-devops-project-manage/references/cli-commands.md +295 -0
- package/skills/dx-devops-project-manage/scripts/create-project.sh +48 -0
- package/skills/dx-devops-project-manage/scripts/list-projects.sh +51 -0
- package/skills/dx-devops-project-manage/scripts/update-project.sh +96 -0
- package/skills/education-cloud-academic-calendar-generate/SKILL.md +225 -0
- package/skills/education-cloud-academic-calendar-generate/examples/quarter-calendar.json +47 -0
- package/skills/education-cloud-academic-calendar-generate/examples/sample-output.md +57 -0
- package/skills/education-cloud-academic-calendar-generate/examples/semester-calendar.json +54 -0
- package/skills/education-cloud-academic-calendar-generate/references/calendar-systems.md +127 -0
- package/skills/education-cloud-academic-calendar-generate/references/date-validation.md +222 -0
- package/skills/education-cloud-academic-calendar-generate/references/foundation_prerequisites.md +40 -0
- package/skills/education-cloud-academic-calendar-generate/scripts/validate_calendar_dates.py +143 -0
- package/skills/education-cloud-course-catalog-migrate/SKILL.md +321 -0
- package/skills/education-cloud-course-catalog-migrate/references/gotchas-detail.md +16 -0
- package/skills/education-cloud-course-catalog-migrate/references/gotchas.md +16 -0
- package/skills/education-cloud-course-catalog-migrate/references/large-catalog-handling.md +42 -0
- package/skills/education-cloud-course-catalog-migrate/scripts/batch_courses.py +36 -0
- package/skills/education-cloud-course-catalog-migrate/scripts/detect_linked_courses.py +51 -0
- package/skills/education-cloud-course-catalog-migrate/scripts/detect_modality_variants.py +48 -0
- package/skills/education-cloud-course-catalog-migrate/scripts/resolve_api_version.py +43 -0
- package/skills/education-cloud-course-catalog-migrate/scripts/split_course_code.py +39 -0
- package/skills/education-cloud-course-catalog-migrate/scripts/validate_completeness.py +54 -0
- package/skills/education-cloud-multi-campus-configure/references/foundation_prerequisites.md +3 -5
- package/skills/education-cloud-student-recruitment-agent-configure/SKILL.md +177 -0
- package/skills/education-cloud-student-recruitment-agent-configure/references/agent-and-subagents.md +151 -0
- package/skills/education-cloud-student-recruitment-agent-configure/references/customer-narration.md +34 -0
- package/skills/education-cloud-student-recruitment-agent-configure/references/execution-model.md +54 -0
- package/skills/education-cloud-student-recruitment-agent-configure/references/flows.md +82 -0
- package/skills/education-cloud-student-recruitment-agent-configure/references/grounding.md +199 -0
- package/skills/education-cloud-student-recruitment-agent-configure/references/permissions.md +183 -0
- package/skills/education-cloud-student-recruitment-agent-configure/references/platform-enablement.md +82 -0
- package/skills/education-cloud-student-recruitment-agent-configure/references/prerequisites.md +158 -0
- package/skills/education-cloud-student-recruitment-agent-configure/references/routing.md +141 -0
- package/skills/experience-cms-brand-apply/SKILL.md +5 -5
- package/skills/experience-cms-brand-create/SKILL.md +2 -2
- package/skills/experience-cms-content-generate/SKILL.md +1 -0
- package/skills/experience-cms-content-render/SKILL.md +173 -0
- package/skills/experience-cms-content-render/assets/angular/DetailPage.component.ts +25 -0
- package/skills/experience-cms-content-render/assets/angular/MediaRenderer.component.ts +133 -0
- package/skills/experience-cms-content-render/assets/angular/TypeList.component.ts +38 -0
- package/skills/experience-cms-content-render/assets/angular/TypeRenderer.component.ts +90 -0
- package/skills/experience-cms-content-render/assets/angular/cms-content.component.ts +248 -0
- package/skills/experience-cms-content-render/assets/angular/cms-item.service.ts +100 -0
- package/skills/experience-cms-content-render/assets/react/DetailPage.tsx +20 -0
- package/skills/experience-cms-content-render/assets/react/MediaRenderer.tsx +129 -0
- package/skills/experience-cms-content-render/assets/react/TypeList.tsx +40 -0
- package/skills/experience-cms-content-render/assets/react/TypeRenderer.tsx +64 -0
- package/skills/experience-cms-content-render/assets/react/heuristicRenderer.tsx +310 -0
- package/skills/experience-cms-content-render/assets/react/useCmsItem.ts +129 -0
- package/skills/experience-cms-content-render/assets/shared/cmsContentType.ts +49 -0
- package/skills/experience-cms-content-render/assets/shared/cmsCore.types.ts +96 -0
- package/skills/experience-cms-content-render/assets/shared/externalRefs.ts +55 -0
- package/skills/experience-cms-content-render/references/bulk-loading.md +60 -0
- package/skills/experience-cms-content-render/references/codegen-guardrails.md +111 -0
- package/skills/experience-cms-content-render/references/detail-pages.md +87 -0
- package/skills/experience-cms-content-render/references/embed-recipes.md +127 -0
- package/skills/experience-cms-content-render/references/failure-modes.md +96 -0
- package/skills/experience-cms-content-render/references/heuristic-render-rules.md +131 -0
- package/skills/experience-cms-content-render/references/init-scaffold.md +122 -0
- package/skills/experience-cms-content-render/references/interaction-model.md +173 -0
- package/skills/experience-cms-content-render/references/package-api.md +106 -0
- package/skills/experience-cms-content-render/references/schema-sync.md +114 -0
- package/skills/experience-cms-content-render/references/styling-scopes.md +65 -0
- package/skills/experience-cms-content-render/references/verify.md +49 -0
- package/skills/experience-cms-content-type-generate/SKILL.md +2 -2
- package/skills/experience-content-media-stock-image-search/SKILL.md +5 -4
- package/skills/experience-search-coordinate/SKILL.md +198 -0
- package/skills/experience-search-coordinate/assets/search-payload-template.json +25 -0
- package/skills/experience-search-coordinate/references/content-route.md +313 -0
- package/skills/experience-search-coordinate/references/content-type-discovery.md +57 -0
- package/skills/experience-search-coordinate/references/media-route.md +172 -0
- package/skills/experience-search-coordinate/references/scope-resolution.md +14 -0
- package/skills/experience-ui-bundle-localize/SKILL.md +1 -1
- package/skills/experience-ui-bundle-localize/references/i18n-setup.md +5 -3
- package/skills/experience-ui-bundle-project-generate/SKILL.md +18 -14
- package/skills/experience-ui-bundle-project-generate/references/angular-project-generate.md +22 -0
- package/skills/experience-ui-bundle-project-generate/references/react-project-generate.md +20 -0
- package/skills/experience-ui-bundle-salesforce-data-access/SKILL.md +58 -54
- package/skills/experience-ui-bundle-salesforce-data-access/references/caching.md +6 -0
- package/skills/experience-ui-bundle-salesforce-data-access/references/graphiti-cli.md +2 -2
- package/skills/experience-ui-bundle-salesforce-data-access/references/migration.md +6 -0
- package/skills/experience-ui-bundle-salesforce-data-access/references/rest-and-integration.md +2 -1
- package/skills/experience-ui-bundle-salesforce-data-access/references/sdk-api.md +6 -0
- package/skills/experience-ui-bundle-site-generate/SKILL.md +59 -8
- package/skills/experience-ui-bundle-site-generate/references/configure-metadata-digital-experience.md +8 -3
- package/skills/experience-ui-bundle-site-generate/references/configure-metadata-language-settings.md +120 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/SKILL.md +336 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/orchestration-flow.md +143 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-application-flexipage-mapping.md +127 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-deploy-commands.md +116 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-lifesci-metadata-deploy.md +111 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-overview.md +312 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-profile-layout-assignments.md +171 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-state-tracking.md +64 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-trigger-handlers.md +122 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-4-user-provisioning-overview.md +335 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-4-user-provisioning-user-provisioning-details.md +140 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-5-visit-creation-execution-state-and-recovery.md +196 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-5-visit-creation-metadata-cache-generation.md +155 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-5-visit-creation-overview.md +307 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-5-visit-creation-visit-creation-data.md +211 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/state-machine-and-changes.md +108 -0
- package/skills/life-sciences-kam-coordinate/SKILL.md +241 -0
- package/skills/life-sciences-kam-coordinate/references/orchestration-flow.md +152 -0
- package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-application-flexipage-mapping.md +79 -0
- package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-deploy-commands.md +131 -0
- package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-kam-config-records.md +85 -0
- package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-lifesci-metadata-deploy.md +112 -0
- package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-overview.md +202 -0
- package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-profile-layout-assignments.md +67 -0
- package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-state-tracking.md +65 -0
- package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-trigger-handlers.md +123 -0
- package/skills/life-sciences-kam-coordinate/references/stage-4-participant-role-and-sprint.md +89 -0
- package/skills/life-sciences-kam-coordinate/references/stage-5-data-and-plan-templates-overview.md +337 -0
- package/skills/life-sciences-kam-coordinate/references/stage-5-data-creation-data.md +248 -0
- package/skills/life-sciences-kam-coordinate/references/stage-6-ipad-validation-script.md +35 -0
- package/skills/life-sciences-kam-coordinate/references/stage-6-metadata-cache-generation.md +155 -0
- package/skills/life-sciences-kam-coordinate/references/stage-6-user-provisioning-details.md +146 -0
- package/skills/life-sciences-kam-coordinate/references/stage-6-user-provisioning-overview.md +89 -0
- package/skills/life-sciences-kam-coordinate/references/state-machine-and-changes.md +114 -0
- package/skills/life-sciences-prerequisites-validate/SKILL.md +138 -0
- package/skills/life-sciences-prerequisites-validate/references/checks-org-settings.md +190 -0
- package/skills/life-sciences-prerequisites-validate/references/checks-user-and-package.md +211 -0
- package/skills/life-sciences-territory-configure/SKILL.md +217 -0
- package/skills/life-sciences-territory-configure/references/territory-metadata.md +262 -0
- package/skills/platform-apex-logs-debug/SKILL.md +7 -7
- package/skills/platform-custom-application-generate/SKILL.md +4 -4
- package/skills/platform-custom-object-generate/SKILL.md +7 -7
- package/skills/platform-custom-tab-generate/SKILL.md +1 -1
- package/skills/platform-dsar-policy-manage/SKILL.md +272 -0
- package/skills/platform-dsar-policy-manage/references/configure.md +106 -0
- package/skills/platform-dsar-policy-manage/references/export-and-history.md +123 -0
- package/skills/platform-dsar-policy-manage/references/gap-analysis-guide.md +150 -0
- package/skills/platform-dsar-policy-manage/references/gap-scan.md +129 -0
- package/skills/platform-dsar-policy-manage/references/headless-sor.md +59 -0
- package/skills/platform-dsar-policy-manage/references/report-format.md +59 -0
- package/skills/platform-dsar-policy-manage/scripts/tests/__init__.py +0 -0
- package/skills/platform-dsar-policy-manage/scripts/tests/test_validate_policy_tree.py +76 -0
- package/skills/platform-dsar-policy-manage/scripts/validate-policy-tree.py +130 -0
- package/skills/platform-flexipage-generate/SKILL.md +4 -0
- package/skills/platform-list-view-generate/SKILL.md +1 -0
- package/skills/platform-salesforce-connect-adapter-generate/SKILL.md +359 -0
- package/skills/platform-salesforce-connect-adapter-generate/references/official-examples.md +69 -0
- package/skills/platform-salesforce-connect-adapter-generate/references/scenarios.md +187 -0
- package/skills/platform-soql-query/SKILL.md +8 -8
- package/skills/platform-value-set-generate/SKILL.md +2 -2
- package/skills/service-itsm-agentic-setup-cmdb-coordinate/SKILL.md +20 -27
- package/skills/service-native-voice-recording-transcription-configure/SKILL.md +47 -27
- package/skills/service-native-voice-recording-transcription-configure/references/thunderbird-voice-settings.md +13 -9
- package/skills/service-native-voice-recording-transcription-configure/scripts/enable-recording-transcription.sh +104 -45
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: education-cloud-course-catalog-migrate
|
|
3
|
+
description: "Use this skill to migrate course catalog data from external sources (CSV, PDF, website) and bulk-create Learning and LearningCourse records in Education Cloud. Triggers when a user wants to import courses, parse course catalog documents, or set up a course catalog for an academic term. Handles multi-source data ingestion, intelligent interpretation of course structures (lecture+lab linked courses via IsLinkedOnly flag, credit hours, course types), batch creation via sObject REST API, data quality validation, conflict resolution for existing courses, and gap identification. Explains what's missing (descriptions, types) and requests additional sources. Pauses for clarification on multi-component courses and conflicting data. DO NOT TRIGGER for creating individual Learning Course records manually, for creating Learning Programs (different workflow), or for enrollment/registration workflows."
|
|
4
|
+
metadata:
|
|
5
|
+
version: "1.0"
|
|
6
|
+
domains:
|
|
7
|
+
- "Education"
|
|
8
|
+
accessCheck:
|
|
9
|
+
- type: "accessCheck"
|
|
10
|
+
value: "IndustriesEducation.userHasEducationCloudAccess"
|
|
11
|
+
cliTools:
|
|
12
|
+
- tool: ["python3"]
|
|
13
|
+
semver: ">=3.9.0"
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Migrating Course Catalog to Education Cloud
|
|
17
|
+
|
|
18
|
+
Import course catalog data from multiple sources (CSV, PDF, website) and bulk-create Learning Course records with structure detection and data quality validation.
|
|
19
|
+
|
|
20
|
+
## Scope
|
|
21
|
+
|
|
22
|
+
- **In scope**: Multi-source ingestion (CSV, PDF, web), Learning + LearningCourse bulk creation via sObject REST API (Composite batches up to 200), linked course detection (lecture+lab via IsLinkedOnly), credit hour parsing (Duration/DurationUnit), course type mapping, data quality validation, conflict resolution, gap identification/remediation
|
|
23
|
+
- **Out of scope**: Creating individual Learning Course records manually (delegate to metadata generation skills), creating Learning Programs (Learning.Type = 'LearningProgram' + separate child object), creating External Learning (Learning.Type = 'ExternalLearning'), course sections/offerings (CourseOffering object), enrollment/registration workflows (no dedicated skill yet — ask the user), SIS sync (separate integration workflow)
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Required Inputs
|
|
28
|
+
|
|
29
|
+
Gather or infer before proceeding:
|
|
30
|
+
|
|
31
|
+
- **Target org**: Active Salesforce org with Education Cloud enabled
|
|
32
|
+
- **Primary data source**: CSV file, PDF document, or website URL with course catalog data
|
|
33
|
+
- **Business Account**: the Academic Operations Business Account to link courses to (e.g., College of Engineering)
|
|
34
|
+
- **Academic term** (optional): context only, not stored on Learning Course
|
|
35
|
+
|
|
36
|
+
Additional sources (gathered during workflow):
|
|
37
|
+
- Supplementary data sources for missing fields (descriptions, types, mappings)
|
|
38
|
+
- Lecture-lab mapping files (if multi-component courses detected)
|
|
39
|
+
- Conflict resolution decisions (if existing courses found)
|
|
40
|
+
|
|
41
|
+
Defaults unless specified:
|
|
42
|
+
- Course status: Active (IsActive = true on Learning)
|
|
43
|
+
- Batch size: 25 records per Composite API call as an agent (API supports 200; tool-call payload limits are the binding constraint — see Step 7)
|
|
44
|
+
- Data quality validation: Enabled
|
|
45
|
+
- Learning.Type: 'LearningCourse' (required for course catalog entries)
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Execution Transport
|
|
50
|
+
|
|
51
|
+
**Resolve the API version first — do not hardcode.** `GET /services/data/` (no version segment) returns an endpoint map already resolved to the org's max version, not a `{version, label, url}` list. Pipe it into `scripts/resolve_api_version.py` to get `{"version": "69.0"}`; use as `vXX.X` everywhere below, including the transport probe.
|
|
52
|
+
|
|
53
|
+
Then probe transport health: `GET /services/data/vXX.X/limits` via headless-360 dispatch. 2xx → route all calls through it. On absent/4xx/5xx → probe other transports (other Salesforce MCP, custom MCP), use first healthy, announce it. None healthy → stop, ask user to connect one; never fabricate. (SOQL `/query` is routable on headless-360 — confirmed by live test; `/tooling/query` and `?fields=` are unconfirmed, use describe + direct record reads instead.)
|
|
54
|
+
|
|
55
|
+
Never fall back to `sf` CLI or shell out for org credentials — a slow/oversized batch is a batch-size problem (shrink it, step 7), not a transport failure, and CLI shell-out trips an unbypassable approval gate on some surfaces. A version too new for the org returns `404 NOT_FOUND` on an otherwise-healthy transport — re-read `GET /services/data/`, drop to the org's max version, retry, don't switch transport. See `references/gotchas.md` for both.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Workflow
|
|
60
|
+
|
|
61
|
+
All steps are sequential. Do not skip or reorder. If blocked, stop and ask for missing context.
|
|
62
|
+
|
|
63
|
+
### Phase 1 — Ingest Primary Source
|
|
64
|
+
|
|
65
|
+
1. **Identify data source type** — determine if primary source is CSV, PDF, or website URL. Ask user to provide the source if not already specified.
|
|
66
|
+
|
|
67
|
+
2. **Parse primary source** — extract course data:
|
|
68
|
+
- For CSV: Use `Read` tool to load file, parse columns
|
|
69
|
+
- For PDF: Use `Read` tool (supports PDF extraction), parse text. Over 20 pages, read in sequential page-range chunks and merge parsed rows before validating completeness — see `references/large-catalog-handling.md` §1
|
|
70
|
+
- For website: Use `WebFetch` tool to retrieve HTML, parse course listings
|
|
71
|
+
|
|
72
|
+
Look for: course codes (identity — split into SubjectAbbreviation + CourseNumber, see step 8/9), course names (Name), credit hours (Duration + DurationUnit), descriptions (Description), course types (Lecture/Lab/Exam/Drill/Recitation), prerequisites, corequisites, linked course indicators (IsLinkedOnly), delivery modality (if the same course code repeats with different Duration/DurationUnit — see `references/large-catalog-handling.md` §2).
|
|
73
|
+
|
|
74
|
+
3. **Validate data completeness** — pipe the parsed records into the completeness helper:
|
|
75
|
+
```bash
|
|
76
|
+
python3 scripts/validate_completeness.py --required "CourseCode,Name,Duration" --recommended "Description,CourseType" <<'JSON'
|
|
77
|
+
[ { "CourseCode": "...", "Name": "...", ... }, ... ]
|
|
78
|
+
JSON
|
|
79
|
+
```
|
|
80
|
+
The report gives present/missing counts per field. Optional fields (prerequisites, corequisites, linked courses) are not checked — their absence never blocks progress.
|
|
81
|
+
|
|
82
|
+
If any required field shows `missing > 0`, report error and stop. If recommended fields are missing, note the gaps and continue to Phase 2.
|
|
83
|
+
|
|
84
|
+
### Phase 2 — Identify Gaps and Request Additional Sources
|
|
85
|
+
|
|
86
|
+
4. **Report data quality** — print a summary table showing which fields are present and which are missing. Example:
|
|
87
|
+
```text
|
|
88
|
+
Data Quality Report:
|
|
89
|
+
• Course codes: 120/120 OK
|
|
90
|
+
• Course names: 120/120 OK
|
|
91
|
+
• Credit hours: 120/120 OK
|
|
92
|
+
• Descriptions: 115/120 (5 missing)
|
|
93
|
+
• Course types: 118/120 (2 missing)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
5. **Request supplementary sources** — if key fields are missing (descriptions, types), ask user: "Do you have additional sources — a website, department document, or mapping file?"
|
|
97
|
+
|
|
98
|
+
- If yes: receive additional source, parse it, and merge data with primary source
|
|
99
|
+
- If no: offer to create courses with placeholder values or stop and wait for complete data
|
|
100
|
+
|
|
101
|
+
Repeat this step until all gaps are filled or user confirms to proceed with incomplete data.
|
|
102
|
+
|
|
103
|
+
If subject-to-Business-Account mapping is ambiguous, resolve every ambiguous case in one pass (source doc + live public catalog + course level/number patterns) and present them together, not one round at a time (see `references/gotchas.md`).
|
|
104
|
+
|
|
105
|
+
6. **Detect multi-component courses** — pipe the course codes into the lecture/lab pairing helper:
|
|
106
|
+
```bash
|
|
107
|
+
python3 scripts/detect_linked_courses.py --suffixes "L,LAB" <<'JSON'
|
|
108
|
+
[ "BIO-201", "BIO-201L", ... ]
|
|
109
|
+
JSON
|
|
110
|
+
```
|
|
111
|
+
Each `{"lecture", "lab"}` pair returned is a candidate multi-component course.
|
|
112
|
+
- If any pairs are found, pause and explain: "I found X multi-component (lecture + lab) courses. Structure them as (1) separate standalone courses, or (2) linked courses (IsLinkedOnly flag, students must take both)?"
|
|
113
|
+
|
|
114
|
+
If user selects "linked courses", request a mapping file or confirm the naming pattern is reliable.
|
|
115
|
+
|
|
116
|
+
**Detect modality duplicates** — group parsed rows by course code. Same code, different `Duration`/`DurationUnit`/`Description` = a modality variant (e.g. Online vs. On-Campus), not a duplicate — never silently dedupe/merge. Resolve the representation once per run, not per-course. See `references/large-catalog-handling.md` §2.
|
|
117
|
+
|
|
118
|
+
### Phase 3 — Batch Create Learning and LearningCourse Records
|
|
119
|
+
|
|
120
|
+
7. **Prepare batch creation plan** — pipe the parsed course records into the batching helper. The Composite API accepts up to 200 records/call, but an agent's tool-call payload has a smaller practical ceiling (~56KB/150 records). Use `--batch-size 25` when executing as an agent (raise toward 200 only if the calling environment confirms it can handle larger payloads):
|
|
121
|
+
```bash
|
|
122
|
+
python3 scripts/batch_courses.py --batch-size 25 <<'JSON'
|
|
123
|
+
[ { "Name": "...", ... }, ... ]
|
|
124
|
+
JSON
|
|
125
|
+
```
|
|
126
|
+
Each element of the returned array is one Composite API call's worth of records. Print: "Starting course catalog import. I'll use Composite API to create records in batches. Total: X courses."
|
|
127
|
+
|
|
128
|
+
If a batch still won't inline through a healthy dispatch transport, re-run with a smaller `--batch-size` (e.g. 10) instead of switching transport. Track dispatch vs. fallback counts and report both in the Phase 5 summary.
|
|
129
|
+
|
|
130
|
+
**Over 200 courses (8+ batches)**: disclose total batch count upfront, print the Batch N complete line after every batch (not just at the end), and — only if the surface exposes a parallel-task/subagent tool — follow the fan-out rules in `references/large-catalog-handling.md` §3-4. Never assume that capability exists; default to sequential batches.
|
|
131
|
+
|
|
132
|
+
8. **Check for existing courses** — before creating Batch 1, scope the check to this batch's own course codes, never a global fetch (an unrelated catalog entry already in the org is not a conflict).
|
|
133
|
+
|
|
134
|
+
Split this batch's course codes first (`scripts/split_course_code.py`) — key the check on those identity fields, never `CipCode` (many orgs leave it null — see `references/gotchas.md`).
|
|
135
|
+
|
|
136
|
+
> **Headless-360 note**: SOQL `/query` is routable via the headless-360 dispatch tool (confirmed by live test) — query `LearningCourse`, matching each split `{SubjectAbbreviation, CourseNumber}` pair exactly (SOQL has no tuple `IN`, so OR the pairs): `SELECT Id, LearningId, Name, SubjectAbbreviation, CourseNumber FROM LearningCourse WHERE (SubjectAbbreviation = 'BIO' AND CourseNumber = '201') OR ...`. If Ids are unknown and a SOQL lookup isn't available, ask the user to supply them (or a prior import manifest). Never do this via Bash/CLI — see `references/gotchas.md`.
|
|
137
|
+
|
|
138
|
+
If any rows come back (i.e. a SubjectAbbreviation+CourseNumber pair in this batch already exists in the org), pause and explain conflict:
|
|
139
|
+
- Show existing course details vs. new import data side-by-side
|
|
140
|
+
- Check if existing courses have dependencies (course offerings, enrollments)
|
|
141
|
+
- Offer options: (1) Update existing courses, (2) Skip existing courses, (3) Cancel and review manually
|
|
142
|
+
|
|
143
|
+
Wait for user decision before proceeding.
|
|
144
|
+
|
|
145
|
+
9. **Create Batch 1** — two-step process for each course:
|
|
146
|
+
|
|
147
|
+
> **Field placement (verified via describe)**: `CipCode`, `Duration`, `DurationUnit`, `ProviderId` are createable on **Learning ONLY** — `ProviderId` on the child fails with `INVALID_FIELD_FOR_INSERT_UPDATE`. LearningCourse takes `Name`, `LearningId`, `CourseType`, `IsLinkedOnly`, `SubjectAbbreviation`, `CourseNumber`, `Description`, `CourseLevelDescription`, `FieldOfStudy` — it inherits the account link via `LearningId`. Confirm with describe before building the body.
|
|
148
|
+
|
|
149
|
+
> **CipCode is opt-in.** Federal CIP taxonomy field, not the course's identity — leave unset unless the source supplies a real CIP code and the user confirms.
|
|
150
|
+
|
|
151
|
+
**Step 1: Create Learning (parent) records via Composite API:**
|
|
152
|
+
```http
|
|
153
|
+
POST /services/data/vXX.X/composite/sobjects
|
|
154
|
+
{
|
|
155
|
+
"allOrNone": false,
|
|
156
|
+
"records": [
|
|
157
|
+
{
|
|
158
|
+
"attributes": {"type": "Learning", "referenceId": "learning1"},
|
|
159
|
+
"Name": "<course-name>",
|
|
160
|
+
"Type": "LearningCourse",
|
|
161
|
+
"Description": "<description>",
|
|
162
|
+
"ProviderId": "<account-id>",
|
|
163
|
+
"Duration": <hours>,
|
|
164
|
+
"DurationUnit": "Credit Hours",
|
|
165
|
+
"IsActive": true
|
|
166
|
+
},
|
|
167
|
+
...
|
|
168
|
+
]
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Capture each returned Id (order matches the `records` array) to use as `LearningId` in Step 2.
|
|
173
|
+
|
|
174
|
+
**Step 2: Create LearningCourse (child) records via Composite API:**
|
|
175
|
+
```http
|
|
176
|
+
POST /services/data/vXX.X/composite/sobjects
|
|
177
|
+
{
|
|
178
|
+
"allOrNone": false,
|
|
179
|
+
"records": [
|
|
180
|
+
{
|
|
181
|
+
"attributes": {"type": "LearningCourse"},
|
|
182
|
+
"Name": "<course-record-name>",
|
|
183
|
+
"LearningId": "<learning-id-from-step-1>",
|
|
184
|
+
"SubjectAbbreviation": "<subject-prefix>",
|
|
185
|
+
"CourseNumber": "<course-number>",
|
|
186
|
+
"CourseType": "<parsed-course-type>",
|
|
187
|
+
"IsLinkedOnly": false
|
|
188
|
+
},
|
|
189
|
+
...
|
|
190
|
+
]
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Set `CourseType` from the parsed/mapped course type for that record (see the Gotchas table for value mapping) — do not hard-code it. A lab record must get `CourseType: "Lab"`, not `"Lecture"`.
|
|
195
|
+
|
|
196
|
+
> **CourseType** picklist values (verified): `Lecture`, `Lab`, `Exam`, `Drill`, `Recitation`.
|
|
197
|
+
|
|
198
|
+
> **`restrictedPicklist: true` means only today's values are valid to submit, not that the set is closed.** If a source value (e.g. `DurationUnit` = "Program Points") isn't in describe's list, don't silently drop/remap it — offer adding the picklist value via metadata alongside remapping or a placeholder (see `references/gotchas.md`).
|
|
199
|
+
|
|
200
|
+
Print progress: `Batch 1 complete — X Learning + X LearningCourse records created`.
|
|
201
|
+
|
|
202
|
+
10. **Handle multi-component courses** — if user selected "linked courses" in step 6:
|
|
203
|
+
- Create lecture courses with `CourseType: "Lecture"`, `IsLinkedOnly = false`
|
|
204
|
+
- Create lab courses with `CourseType: "Lab"`, `IsLinkedOnly = true`
|
|
205
|
+
- Use the `detect_linked_courses.py` pairs (or a user-supplied mapping file) to determine which labs are linked-only
|
|
206
|
+
- Print: `X linked lab courses created (IsLinkedOnly = true)`
|
|
207
|
+
|
|
208
|
+
11. **Repeat for remaining batches** — continue steps 8-10 for Batch 2, Batch 3, etc. until all courses created.
|
|
209
|
+
|
|
210
|
+
### Phase 4 — Data Quality Validation and Conflict Resolution
|
|
211
|
+
|
|
212
|
+
12. **Scan for incomplete records** — after all batches complete, read back each newly created record by the Id captured from its create response:
|
|
213
|
+
```http
|
|
214
|
+
GET /services/data/vXX.X/sobjects/Learning/<id>
|
|
215
|
+
GET /services/data/vXX.X/sobjects/LearningCourse/<id>
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
> **Headless-360 note**: SOQL `/query` is routable via the headless-360 dispatch tool (confirmed by live test) — a `CreatedDate = TODAY` query works as an alternative to iterating captured Ids with direct record GETs.
|
|
219
|
+
|
|
220
|
+
Run the completeness helper **separately per sObject** — `CourseType` exists only on `LearningCourse`, `Description` only on `Learning`; combining both shapes into one call false-positives every parent as missing `CourseType`:
|
|
221
|
+
```bash
|
|
222
|
+
python3 scripts/validate_completeness.py --required "Name" --recommended "Description" <<'JSON'
|
|
223
|
+
[ { "Name": "...", "Description": "...", ... }, ... ] # Learning read-backs only
|
|
224
|
+
JSON
|
|
225
|
+
```
|
|
226
|
+
Repeat with `--recommended "CourseType"` against LearningCourse read-backs only. Identify courses with missing descriptions (Learning report) or missing types (LearningCourse report). Print data quality report (same format as step 4).
|
|
227
|
+
|
|
228
|
+
13. **Detect conflicting data** — if multiple sources provided different values for the same course, flag conflicts, e.g. "CHEM-301: website says X, PDF says Y — which should I use?" Wait for user decision, then update the conflicting courses.
|
|
229
|
+
|
|
230
|
+
14. **Remediate gaps** — if user provides additional sources to fill gaps, parse them and update the incomplete courses:
|
|
231
|
+
```http
|
|
232
|
+
PATCH /services/data/vXX.X/sobjects/Learning/<id>
|
|
233
|
+
{"Description": "..."}
|
|
234
|
+
|
|
235
|
+
PATCH /services/data/vXX.X/sobjects/LearningCourse/<id>
|
|
236
|
+
{"CourseType": "<parsed-course-type>"}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Phase 5 — Summary and Verification
|
|
240
|
+
|
|
241
|
+
15. **Generate import summary** — print final report:
|
|
242
|
+
```text
|
|
243
|
+
Course catalog import finished!
|
|
244
|
+
Summary:
|
|
245
|
+
• X Learning + X LearningCourse records created
|
|
246
|
+
• Y standalone, Z linked lab courses (IsLinkedOnly)
|
|
247
|
+
• All courses linked to <Business Account name>, set Active
|
|
248
|
+
Data sources used: 1. <primary> 2. <supplementary 1> 3. <supplementary 2>
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**If any gaps were remediated from a supplementary source (step 5/14 ran)**, the final report MUST also include, in this order:
|
|
252
|
+
- **Data Quality Report** — two tables, "Initial data (from `<primary source>`)" and "After supplementary source (`<supplementary source>`)", each listing every field from step 4 with its present/total count. This is the before/after pair, not just the final counts.
|
|
253
|
+
- **Gap Remediation** — grouped by field (e.g. "Courses with missing descriptions (filled from `<source>`)"), a numbered list of every remediated course with the value it received. One list per remediated field.
|
|
254
|
+
|
|
255
|
+
16. **Offer verification** — ask user: "Would you like me to query specific courses to verify the data? For example, I can show you a course with its LearningCourse child record, or check for any remaining data quality issues."
|
|
256
|
+
|
|
257
|
+
17. **Check the running user can actually see what was just created** — don't just flag this, check it (see `references/gotchas.md` for the resolve-running-user + FLS/RecordTypeVisibility pattern), against every object touched (`Learning`, `LearningCourse`, `Account` if Person Accounts came up). Name any gap and its profile, offer the specific fix, and wait for confirmation before writing `Profile`/`PermissionSet` metadata. Default-Private OWD/sharing is org-wide, not per-user — flag it, don't offer to change it.
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## Rules / Constraints
|
|
262
|
+
|
|
263
|
+
| Constraint | Rationale |
|
|
264
|
+
|-----------|-----------|
|
|
265
|
+
| Use Composite API, batched at 25 records/call for agent execution (API supports up to 200) | sObject REST standard in H360 MCP; a 150-record payload (~56KB) exceeds typical tool-call limits |
|
|
266
|
+
| Create Learning first, then LearningCourse | LearningCourse.LearningId requires parent to exist first |
|
|
267
|
+
| Always set Learning.Type = 'LearningCourse' | Learning is polymorphic — Type distinguishes courses from programs |
|
|
268
|
+
| Always query for existing courses before creating | Prevents duplicates; lets user decide on conflicts |
|
|
269
|
+
| Pause for clarification on multi-component courses | IsLinkedOnly flag affects enrollment behavior — user must confirm structure |
|
|
270
|
+
| Report data quality gaps before proceeding | Lets user provide complete data before committing to org |
|
|
271
|
+
| Wait for user decision on conflicting data | Agent cannot determine which source is authoritative |
|
|
272
|
+
| Do not invent missing data | Placeholder values must be explicitly approved by user |
|
|
273
|
+
| Set ProviderId (Business Account) on the parent Learning ONLY | Createable on Learning, not LearningCourse — see step 9 field-placement note |
|
|
274
|
+
| Set all courses to Active status by default (IsActive = true) | Must be Active to appear in catalog search/registration |
|
|
275
|
+
| Use sObject REST API, not CLI commands for org calls | H360 MCP skills have no execution environment for reaching the org. The `scripts/` helpers are local data transforms (no org call) — if no Bash tool is available, perform the equivalent check inline instead of skipping it |
|
|
276
|
+
| Use tool-agnostic language | Skill ships to multiple AI surfaces beyond Claude Code |
|
|
277
|
+
| Over 200 courses or 20+ source pages: follow `references/large-catalog-handling.md`, not ad hoc chunking | Improvised chunking risks split-page data loss or duplicate creates |
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## Gotchas
|
|
282
|
+
|
|
283
|
+
See `references/gotchas.md` for known failure modes and resolutions (parsing quirks, missing Business Account, picklist mapping, partial Composite failures, and more).
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## Output Expectations
|
|
288
|
+
|
|
289
|
+
Deliverables:
|
|
290
|
+
- X Learning records created (Type = 'LearningCourse')
|
|
291
|
+
- X LearningCourse child records created (LearningId lookups)
|
|
292
|
+
- Data quality report showing completeness per field
|
|
293
|
+
- Import summary listing all data sources used
|
|
294
|
+
- Optional: course verification via direct record reads or SOQL `/query` (both routable via headless-360)
|
|
295
|
+
|
|
296
|
+
No files are produced — all output is org records created via sObject REST API.
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## Cross-Skill Integration
|
|
301
|
+
|
|
302
|
+
| Need | Delegate to |
|
|
303
|
+
|------|-------------|
|
|
304
|
+
| Create individual Learning Course manually | Platform metadata generation skills |
|
|
305
|
+
| Create Learning Programs | No dedicated skill yet — ask the user |
|
|
306
|
+
| Set up course sections/offerings | No dedicated skill yet — ask the user |
|
|
307
|
+
| Configure enrollment eligibility rules | No dedicated skill yet — ask the user |
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## Reference File Index
|
|
312
|
+
|
|
313
|
+
- `references/gotchas.md` — known failure modes and their resolutions
|
|
314
|
+
- `references/gotchas-detail.md` — lower-frequency gotchas, linked from gotchas.md
|
|
315
|
+
- `references/large-catalog-handling.md` — rules for >200 courses or >20-page source docs: paginated reads, modality-duplicate detection, checkpointing, gated parallel fan-out (steps 2, 6, 7)
|
|
316
|
+
- `scripts/resolve_api_version.py` — extracts org's max API version from the headless-360 endpoint map (Execution Transport)
|
|
317
|
+
- `scripts/validate_completeness.py` — required/recommended field presence check (steps 3, 12)
|
|
318
|
+
- `scripts/detect_linked_courses.py` — lecture/lab course-code pairing (step 6)
|
|
319
|
+
- `scripts/detect_modality_variants.py` — flags CourseCode rows differing by Duration/DurationUnit/Description, for `large-catalog-handling.md` §2
|
|
320
|
+
- `scripts/batch_courses.py` — Composite API batch splitting (step 7)
|
|
321
|
+
- `scripts/split_course_code.py` — SubjectAbbreviation/CourseNumber splitting (step 9)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Gotchas — edge cases
|
|
2
|
+
|
|
3
|
+
Lower-frequency gotchas moved out of the core `gotchas.md` table to keep it scannable. Same
|
|
4
|
+
resolution format, just less common in a typical run.
|
|
5
|
+
|
|
6
|
+
| Issue | Resolution |
|
|
7
|
+
|-------|------------|
|
|
8
|
+
| Linked course naming pattern is not reliable | Always request a mapping file if user selects "linked courses" option |
|
|
9
|
+
| Existing courses have enrollments | Warn user that updating courses with active enrollments may impact students — offer to skip instead |
|
|
10
|
+
| User provides incomplete data and wants to proceed | Explicitly confirm with user: "5 courses will be created without descriptions. Proceed?" |
|
|
11
|
+
| Bulk import creates thousands of records under default Private sharing, or Person Accounts is enabled without the running profile having record-type visibility | Not this skill's job to change sharing/security settings, but flag it once at the end of the run: recommend the user (or an admin) review OWD/sharing rules and record-type visibility on the objects just populated, since a large private-shared import can be invisible to other admins/agents |
|
|
12
|
+
| describe pulls the full object schema (tens of KB) just to check one field's picklist values | Call describe once per object per run and cache the result (picklist values, createable fields) for reuse across every subsequent lookup on that object instead of re-describing per field/record |
|
|
13
|
+
| A subject code's department/account mapping is ambiguous (shared prefixes, courses left in the catalog from a retired program) | Before guessing or asking the user to correct it round-by-round, cross-check every ambiguous mapping in one pass: the source document, the institution's live public catalog if one exists, and course level/number patterns (e.g. HRM vs. general Business) — surfacing all ambiguous cases together with your proposed resolution up front costs one round of user review instead of several |
|
|
14
|
+
| Running user can't see records/values just created (recurring pattern — reported across multiple test sessions, not unique to this skill) | Metadata creates (new field, object, record type, or a picklist value on a record-type-scoped business process) don't auto-grant visibility to any profile. Resolve the running user via `GET /services/data/vXX.X/` → parse the `identity` URL's trailing `/<userId>` → `SELECT ProfileId, Profile.Name FROM User WHERE Id = '<userId>'`. Then check the relevant grant for what changed: a new/changed field → `FieldPermissions`; a record type touched (e.g. Person Account enabled in a prior session) → `RecordTypeVisibility`; a new object → `ObjectPermissions`. Name the exact gap and profile, offer the specific fix, and wait for confirmation before writing `Profile`/`PermissionSet` metadata — it's profile-wide, not scoped to this run |
|
|
15
|
+
| Adding a picklist value fixes the *global* value set but not necessarily every record type's available values | If the picklist field is record-type-scoped (a business process / per-record-type value set), a newly added value may still be unavailable on record types that existed before the value was added. Check record-type-specific availability, not just the field's global `restrictedPicklist` value list, before declaring the fix complete |
|
|
16
|
+
| Agent stalls indefinitely (no error, no progress) on ANY shelled-out org call — an ad hoc existing-record check (e.g. "check which CipCodes already exist"), or fetching org credentials via "sf org display" to hand-roll direct REST/Composite calls | Some agent surfaces show a manual "approval needed even in bypass" gate for any shell command containing `$(...)`, backticks, brace expansion, pipes into `python3`/`jq`, or eval — a gate that cannot be auto-approved and has no timeout of its own. Never shell out (CLI + command substitution, or CLI piped into a parser) for org lookups or credentials, even as a "faster" alternative to the MCP dispatch tool; use the headless-360 dispatch tool's SOQL `/query` or a direct REST read instead — same result, no approval gate, no CLI dependency |
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Gotchas
|
|
2
|
+
|
|
3
|
+
| Issue | Resolution |
|
|
4
|
+
|-------|------------|
|
|
5
|
+
| CSV column headers don't match expected names | Infer column mapping from context (e.g., "Code" → course code, split via `scripts/split_course_code.py` into SubjectAbbreviation/CourseNumber — not CipCode, see step 8/9; "Title" → Name, "Credits" → Duration) |
|
|
6
|
+
| Credit hours stored as "3.0 credits" instead of numeric | Parse and extract numeric value, set DurationUnit = "Credit Hours" |
|
|
7
|
+
| Course codes have inconsistent formatting | Normalize before creating (e.g., "BIO 201" → "BIO-201", "bio-201" → "BIO-201") |
|
|
8
|
+
| PDF has multi-column layout | Text extraction may be out of order — validate course sequence makes sense |
|
|
9
|
+
| Website has pagination | Fetch all pages before parsing (detect "next page" links and loop) |
|
|
10
|
+
| Missing Business Account ID | Ask the user to supply the Account Id (ProviderId), read a known Account with `GET /services/data/vXX.X/sobjects/Account/<id>`, or look it up by name with a SOQL `/query` (confirmed routable via headless-360) |
|
|
11
|
+
| Course type values don't match Education Cloud picklist | Map common values (e.g., "Lec" → "Lecture", "Laboratory" → "Lab", "Drill" → "Drill", "Recitation" → "Recitation") |
|
|
12
|
+
| Composite API returns partial success | Check response for each record — some may fail while others succeed (allOrNone = false) |
|
|
13
|
+
| Two-object create pattern complexity | Create Learning records first (store IDs), then create LearningCourse records with LearningId lookups |
|
|
14
|
+
| Source value not in describe's picklist list (e.g. DurationUnit = "Program Points") | `restrictedPicklist: true` means only active-today values are valid, not that the set is closed — offer the user the option to add the new picklist value via metadata, in addition to remapping or placeholder options. Do not assume it's a dead end |
|
|
15
|
+
|
|
16
|
+
More edge-case gotchas (linked-course mapping, enrollment conflicts, sharing defaults, describe caching, ambiguous subject codes, post-write visibility, record-type-scoped picklists, shell-out stalls): see `references/gotchas-detail.md`.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Large Catalog Handling
|
|
2
|
+
|
|
3
|
+
Applies whenever the parsed course count exceeds **200 courses** (8+ batches at the standard batch size of 25) OR the primary source document exceeds **20 pages**. Below these thresholds, follow the standard Workflow in `SKILL.md` as written — nothing here overrides it.
|
|
4
|
+
|
|
5
|
+
## 1. Large source documents (PDF/website)
|
|
6
|
+
|
|
7
|
+
- The `Read` tool caps large PDFs at **~20 pages per call** and requires an explicit page range above 10 pages. Never attempt a single `Read` on a 100+ page catalog PDF.
|
|
8
|
+
- Read in sequential page-range chunks (e.g. `pages: "1-20"`, `"21-40"`, ...). Merge parsed course rows across chunks before running the completeness check (step 3) — do not validate completeness per-chunk, a course description that spans a page boundary will false-positive as missing.
|
|
9
|
+
- For a paginated website catalog, the existing "fetch all pages before parsing" rule (`references/gotchas.md`) already covers this — no change, just note it applies here too.
|
|
10
|
+
- State the total page/chunk count to the user before starting the read pass: `"Source document is 640 pages — I'll read it in 32 chunks of 20 pages."` Silent chunking with no upfront count leaves the user unable to judge how long ingestion will take.
|
|
11
|
+
|
|
12
|
+
## 2. Modality / delivery-method duplicates
|
|
13
|
+
|
|
14
|
+
Real catalogs sometimes list the same course twice under different delivery modalities (e.g. Online vs. On-Campus), each with its own `Duration`/`DurationUnit`. This is a **source-data shape issue**, detected during Phase 2 parsing — distinct from the step 8 org-conflict check, which only looks at records *already in the org*.
|
|
15
|
+
|
|
16
|
+
- After parsing, run `scripts/detect_modality_variants.py` on the parsed rows (JSON array with `CourseCode`, `Duration`, `DurationUnit`, `Description` per row). It groups by `CourseCode` and flags any group whose rows differ in those fields. Use its flagged output for the representation decision below — never silently dedupe (drop one) or silently merge (average/pick one) a flagged group.
|
|
17
|
+
- Before assuming there's no dedicated field for the distinction, run `GET /services/data/vXX.X/sobjects/Learning/describe` and check for a delivery-method/modality field. Education Cloud orgs vary — do not assume absence without checking.
|
|
18
|
+
- If no such field exists on the describe result, pause and ask the user how to distinguish the variants once (not per-course): typically either (a) separate `Learning` records with the modality appended to `Name` (e.g. `"BIO-201 (Online)"`), or (b) a single record if the org treats modality as an offering-level (not course-level) attribute. Apply the chosen pattern to every flagged variant in this run, and state the pattern in the Phase 5 summary.
|
|
19
|
+
|
|
20
|
+
## 3. Batch-count disclosure and checkpointing
|
|
21
|
+
|
|
22
|
+
- Before Phase 3 begins, tell the user the total course count and batch count: `"1,438 courses parsed. This will run as 58 batches of 25 (116 Composite API calls). I'll report progress after every batch."`
|
|
23
|
+
- Print the existing `Batch N complete — X Learning + X LearningCourse records created` line (step 9) after **every** batch, not just at the end — this is the checkpoint. The step 8 existing-course check is what makes a checkpoint useful: if this run is interrupted and re-invoked, step 8 will find the already-created courses and skip them, so print it as an explicit resume contract to the user rather than only as a conflict check: `"Resuming — found 425 of 1,438 courses already created from a prior run, continuing from course 426."`
|
|
24
|
+
- Do not build a separate progress-manifest file. The org itself (queryable via step 8) is the source of truth for what's done — a local manifest can drift from it after an interruption and is one more thing to keep in sync.
|
|
25
|
+
|
|
26
|
+
## 4. Parallel batch fan-out (optional acceleration)
|
|
27
|
+
|
|
28
|
+
Only attempt this when the current surface exposes a capability to run independent units of work concurrently (e.g. Claude Code's `Agent`/Task tool). **Never assume this capability exists** — Agentforce, plain MCP surfaces, and the ADK eval harness's `vibes-cli` runtime do not expose it. If no such capability is available, run batches sequentially per the standard Workflow — this is the default, not a degraded fallback.
|
|
29
|
+
|
|
30
|
+
When available, follow these rules exactly:
|
|
31
|
+
|
|
32
|
+
1. **Fan-out unit is one whole batch**, never a half-batch. A batch's Learning-create-then-LearningCourse-create sequence (step 9) is strictly ordered — the child create needs the parent's returned Id. Splitting a batch's two steps across workers is not allowed.
|
|
33
|
+
2. **Partition before fan-out, by index, not content.** Divide the full course list into disjoint batches up front (course 1-25 → batch 1, 26-50 → batch 2, ...). Two workers must never receive overlapping course records.
|
|
34
|
+
3. **Run step 8 (existing-course check) once, globally, before fan-out** — never per-worker. A per-worker check races: two workers can both see "no conflict" for the same course code in the same instant and both create it.
|
|
35
|
+
4. **Cap concurrency at 4 workers.** Do not fan out all batches at once — Salesforce enforces per-org concurrent long-running-request limits; unbounded fan-out risks `REQUEST_LIMIT_EXCEEDED` that a sequential run would never hit. Launch the next batch only when a worker slot frees up.
|
|
36
|
+
5. **Each worker's prompt must be fully self-contained.** A freshly spawned worker does not see this conversation. Include in its prompt: the exact course-record slice (not a range to look up), the resolved API version, the `ProviderId`, the field-placement rule that `ProviderId`/`Duration`/`DurationUnit` go on `Learning` only, never on `LearningCourse` (and that `CipCode` is opt-in — leave unset unless the source supplies a real CIP code), and that the existing-course conflict check keys on `LearningCourse.SubjectAbbreviation`+`CourseNumber`, never `Learning.CipCode` — restating the gotcha per-worker is cheaper than a worker rediscovering it via a failed create.
|
|
37
|
+
6. **Aggregate before Phase 4/5 — this is a hard barrier.** Collect every worker's created-Ids-and-errors list before running the Phase 4 data quality scan or the Phase 5 summary. Do not run quality validation on a partial result set.
|
|
38
|
+
7. **Isolate failures per batch.** A failed worker must not abort sibling workers. Track outcome per batch (`succeeded` / `failed` / `partial`) and report each in the Phase 5 summary — do not collapse to a single aggregate count when any batch failed or partially failed.
|
|
39
|
+
|
|
40
|
+
### Verification status
|
|
41
|
+
|
|
42
|
+
This fan-out path is **not covered by the current ADK eval harness** — the eval runtime (`vibes-cli` surface) has no subagent/parallel-task tool, the same class of gap as headless-360's absence there. Verification for this path is manual, against real large catalogs (e.g. a 1,438-course, two-modality catalog), not automated eval — until that manual pass is done and recorded, treat this as unverified best-effort, and say so plainly if asked whether it's covered by CI.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Split a list of course records into fixed-size batches.
|
|
3
|
+
|
|
4
|
+
Reads a JSON array of course record objects from stdin. Prints a JSON array
|
|
5
|
+
of batches (each a list of records), each capped at --batch-size records —
|
|
6
|
+
the Composite API's per-call record limit.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python3 scripts/batch_courses.py --batch-size 200
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import json
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def batch(records: list[dict], size: int) -> list[list[dict]]:
|
|
20
|
+
return [records[i : i + size] for i in range(0, len(records), size)]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main() -> int:
|
|
24
|
+
parser = argparse.ArgumentParser()
|
|
25
|
+
parser.add_argument("--batch-size", type=int, default=200)
|
|
26
|
+
args = parser.parse_args()
|
|
27
|
+
|
|
28
|
+
records = json.load(sys.stdin)
|
|
29
|
+
batches = batch(records, args.batch_size)
|
|
30
|
+
json.dump(batches, sys.stdout, indent=2)
|
|
31
|
+
print()
|
|
32
|
+
return 0
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
if __name__ == "__main__":
|
|
36
|
+
sys.exit(main())
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Pair lecture/lab course codes by suffix.
|
|
3
|
+
|
|
4
|
+
Reads a JSON array of course code strings from stdin. A code is treated as a
|
|
5
|
+
lab section of another code if it equals that code plus one of the configured
|
|
6
|
+
suffixes (default: L, LAB). Prints a JSON array of {"lecture", "lab"} pairs
|
|
7
|
+
for every match found; codes with no match are omitted.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
python3 scripts/detect_linked_courses.py --suffixes L,LAB,-L
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import sys
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def normalize(code: str) -> str:
|
|
21
|
+
return code.strip().upper()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def detect(codes: list[str], suffixes: list[str]) -> list[dict]:
|
|
25
|
+
normalized = {normalize(c): c for c in codes}
|
|
26
|
+
pairs = []
|
|
27
|
+
for norm_code, original in normalized.items():
|
|
28
|
+
for suffix in suffixes:
|
|
29
|
+
if norm_code.endswith(suffix) and len(norm_code) > len(suffix):
|
|
30
|
+
base = norm_code[: -len(suffix)]
|
|
31
|
+
if base in normalized:
|
|
32
|
+
pairs.append({"lecture": normalized[base], "lab": original})
|
|
33
|
+
return pairs
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def main() -> int:
|
|
37
|
+
parser = argparse.ArgumentParser()
|
|
38
|
+
parser.add_argument("--suffixes", default="L,LAB", help="Comma-separated lab-code suffixes")
|
|
39
|
+
args = parser.parse_args()
|
|
40
|
+
|
|
41
|
+
codes = json.load(sys.stdin)
|
|
42
|
+
suffixes = [s.strip().upper() for s in args.suffixes.split(",") if s.strip()]
|
|
43
|
+
|
|
44
|
+
pairs = detect(codes, suffixes)
|
|
45
|
+
json.dump(pairs, sys.stdout, indent=2)
|
|
46
|
+
print()
|
|
47
|
+
return 0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
sys.exit(main())
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Flag rows that share a CourseCode but differ in modality-bearing fields.
|
|
3
|
+
|
|
4
|
+
Reads a JSON array of course row objects from stdin, each with at least
|
|
5
|
+
"CourseCode" and optionally "Duration", "DurationUnit", "Description". Groups
|
|
6
|
+
rows by CourseCode; any group with 2+ rows that differ in Duration,
|
|
7
|
+
DurationUnit, or Description is a modality variant (e.g. Online vs.
|
|
8
|
+
On-Campus). Prints a JSON array of {"CourseCode", "rows"} for every flagged
|
|
9
|
+
group; groups with no variance are omitted.
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
python3 scripts/detect_modality_variants.py < courses.json
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import sys
|
|
19
|
+
from collections import defaultdict
|
|
20
|
+
|
|
21
|
+
VARIANT_FIELDS = ("Duration", "DurationUnit", "Description")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def detect(rows: list[dict]) -> list[dict]:
|
|
25
|
+
grouped: dict[str, list[dict]] = defaultdict(list)
|
|
26
|
+
for row in rows:
|
|
27
|
+
grouped[row["CourseCode"]].append(row)
|
|
28
|
+
|
|
29
|
+
flagged = []
|
|
30
|
+
for code, group in grouped.items():
|
|
31
|
+
if len(group) < 2:
|
|
32
|
+
continue
|
|
33
|
+
distinct = {tuple(row.get(f) for f in VARIANT_FIELDS) for row in group}
|
|
34
|
+
if len(distinct) > 1:
|
|
35
|
+
flagged.append({"CourseCode": code, "rows": group})
|
|
36
|
+
return flagged
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def main() -> int:
|
|
40
|
+
rows = json.load(sys.stdin)
|
|
41
|
+
flagged = detect(rows)
|
|
42
|
+
json.dump(flagged, sys.stdout, indent=2)
|
|
43
|
+
print()
|
|
44
|
+
return 0
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
sys.exit(main())
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Extract the org's current max REST API version from a headless-360 endpoint map.
|
|
3
|
+
|
|
4
|
+
Reads the JSON object returned by `GET /services/data/` (no version segment) from
|
|
5
|
+
stdin — a map of endpoint name to path, e.g. {"limits": "/services/data/v69.0/limits",
|
|
6
|
+
...}, not a `{version, label, url}` list. Scans every value for a `vNN.N` segment and
|
|
7
|
+
prints the highest one found as {"version": "69.0"}.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
python3 scripts/resolve_api_version.py < endpoint_map.json
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import re
|
|
17
|
+
import sys
|
|
18
|
+
|
|
19
|
+
VERSION_RE = re.compile(r"v(\d+\.\d+)")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def resolve(endpoint_map: dict) -> str:
|
|
23
|
+
versions = set()
|
|
24
|
+
for value in endpoint_map.values():
|
|
25
|
+
if isinstance(value, str):
|
|
26
|
+
match = VERSION_RE.search(value)
|
|
27
|
+
if match:
|
|
28
|
+
versions.add(match.group(1))
|
|
29
|
+
if not versions:
|
|
30
|
+
raise ValueError("No vNN.N version found in any endpoint path")
|
|
31
|
+
return max(versions, key=lambda v: tuple(int(p) for p in v.split(".")))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def main() -> int:
|
|
35
|
+
endpoint_map = json.load(sys.stdin)
|
|
36
|
+
version = resolve(endpoint_map)
|
|
37
|
+
json.dump({"version": version}, sys.stdout)
|
|
38
|
+
print()
|
|
39
|
+
return 0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
sys.exit(main())
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Split course codes into SubjectAbbreviation + CourseNumber.
|
|
3
|
+
|
|
4
|
+
Reads a JSON array of course code strings from stdin (e.g. "BIO-201",
|
|
5
|
+
"bio 201l"). Prints a JSON array of {"code", "SubjectAbbreviation",
|
|
6
|
+
"CourseNumber"} objects — LearningCourse has no single CipCode field, so
|
|
7
|
+
these two fields must be populated separately.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
python3 scripts/split_course_code.py
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import re
|
|
17
|
+
import sys
|
|
18
|
+
|
|
19
|
+
PATTERN = re.compile(r"^\s*([A-Za-z]+)[\s-]*([0-9]+[A-Za-z]*)\s*$")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def split(code: str) -> dict:
|
|
23
|
+
match = PATTERN.match(code)
|
|
24
|
+
if not match:
|
|
25
|
+
return {"code": code, "SubjectAbbreviation": None, "CourseNumber": None}
|
|
26
|
+
subject, number = match.groups()
|
|
27
|
+
return {"code": code, "SubjectAbbreviation": subject.upper(), "CourseNumber": number.upper()}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def main() -> int:
|
|
31
|
+
codes = json.load(sys.stdin)
|
|
32
|
+
results = [split(c) for c in codes]
|
|
33
|
+
json.dump(results, sys.stdout, indent=2)
|
|
34
|
+
print()
|
|
35
|
+
return 0
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
if __name__ == "__main__":
|
|
39
|
+
sys.exit(main())
|