diff --git a/README.md b/README.md index 1a95493..70b6b69 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,12 @@ reproducing Microsoft Learn pages. ## Project status -The project is in its first reader milestone. The repository provides separate +The project has completed its reader-foundation milestone. The repository provides separate public reader and private administration application factories, a validated exam-agnostic book contract, the current DP-700 outline, sanitized Markdown -rendering, a responsive reader shell, automated boundary tests, and CI. Reader -preferences, local progress, annotations, and offline support remain roadmap -work. +rendering, a responsive reader shell, persistent appearance and progress state, +an official-source registry, automated boundary tests, and CI. Annotations, +offline support, and the refresh/publishing pipeline remain roadmap work. ## Architecture boundary diff --git a/content/published/dp700/book.yaml b/content/published/dp700/book.yaml index 578e085..5e4afda 100644 --- a/content/published/dp700/book.yaml +++ b/content/published/dp700/book.yaml @@ -7,6 +7,57 @@ sources: - id: dp700-study-guide title: Official DP-700 study guide url: https://learn.microsoft.com/en-us/credentials/certifications/resources/study-guides/dp-700 + retrieved_at: 2026-09-02T04:45:35Z + content_sha256: 97f5520c88cb9cc9833c1cebe2bb61d899f12825ac97dc2198584d683f3e5596 + chapter_ids: [workspace-settings, lifecycle-management, security-governance, orchestration, loading-patterns, batch-data, streaming-data, monitor-items, resolve-errors, optimize-performance] + - id: onelake-overview + title: What is OneLake? + url: https://learn.microsoft.com/en-us/fabric/onelake/onelake-overview + retrieved_at: 2026-09-02T04:45:35Z + content_sha256: fd6c5687b64f131db16d4264dbeedc37ec110a52ff13b487e5c9e1ad150c1ebb + chapter_ids: [workspace-settings] + - id: onelake-diagnostics + title: OneLake diagnostics + url: https://learn.microsoft.com/en-us/fabric/onelake/onelake-diagnostics-overview + retrieved_at: 2026-09-02T04:45:35Z + content_sha256: 3cd73c6ff5f198a7faf47f308aa3f58bc649bc1cebd3e215f958b43200cb7bb5 + chapter_ids: [workspace-settings] + - id: onelake-storage-tiers + title: Storage tiers in OneLake + url: https://learn.microsoft.com/en-us/fabric/onelake/onelake-storage-tiers + retrieved_at: 2026-09-02T04:45:35Z + content_sha256: 3e6d11876c66873d0cb452a5fe2cef02fdab3782a1400724b758a202f47d2120 + chapter_ids: [workspace-settings] + - id: onelake-lifecycle + title: OneLake lifecycle management + url: https://learn.microsoft.com/en-us/fabric/onelake/onelake-lifecycle-management + retrieved_at: 2026-09-02T04:45:35Z + content_sha256: 261d43b3f34bfbc66a472a0f32e9e7d3de5e57801018785d7ef50d1b57311c0a + chapter_ids: [workspace-settings] + - id: onelake-settings-api + title: OneLake Settings - Get Settings REST API + url: https://learn.microsoft.com/en-us/rest/api/fabric/core/onelake-settings/get-settings + retrieved_at: 2026-09-02T04:45:35Z + content_sha256: 5f6c3efbc8db82e5bf7e6864286f2eb9141f7026e02efcdf9eb15252629e28ea + chapter_ids: [workspace-settings] + - id: spark-compute-settings + title: Spark compute configuration settings in Fabric environments + url: https://learn.microsoft.com/en-us/fabric/data-engineering/environment-manage-compute + retrieved_at: 2026-09-02T05:18:00Z + content_sha256: 5983dc3981f7db4a3d2335b9bfe1d2eb5546b8248fd20704cd96fa6f7af02ba2 + chapter_ids: [workspace-settings] + - id: fabric-domains + title: Fabric domains + url: https://learn.microsoft.com/en-us/fabric/governance/domains + retrieved_at: 2026-09-02T05:18:00Z + content_sha256: c8ba21672293a996fadc9669ee4116a6324733f5b616d762417d237b2d56c85e + chapter_ids: [workspace-settings] + - id: airflow-workspace-settings + title: Apache Airflow Job workspace settings + url: https://learn.microsoft.com/en-us/fabric/data-factory/apache-airflow-jobs-workspace-settings + retrieved_at: 2026-09-02T05:18:00Z + content_sha256: 058c6d6feb30c2984ff481f9719866b4df8ae245a6497dcb232cc9a22df243fb + chapter_ids: [workspace-settings] domains: - id: implement-manage title: Implement and manage an analytics solution @@ -16,8 +67,8 @@ domains: slug: workspace-settings title: Configure Microsoft Fabric workspace settings content_path: workspace-settings.md - status: draft - source_ids: [dp700-study-guide] + status: published + source_ids: [dp700-study-guide, spark-compute-settings, fabric-domains, onelake-overview, onelake-diagnostics, onelake-storage-tiers, onelake-lifecycle, onelake-settings-api, airflow-workspace-settings] objectives: - {id: implement.workspace.spark, title: Configure Spark workspace settings} - {id: implement.workspace.domain, title: Configure domain workspace settings} diff --git a/content/published/dp700/chapters/workspace-settings.md b/content/published/dp700/chapters/workspace-settings.md index 127e7fd..1d1c4d4 100644 --- a/content/published/dp700/chapters/workspace-settings.md +++ b/content/published/dp700/chapters/workspace-settings.md @@ -1,42 +1,247 @@ # Configure Microsoft Fabric workspace settings -Workspace settings are control points. They shape how a team uses compute, organizes ownership, reaches shared data, and schedules orchestration. For the exam, focus on **who controls a setting, what scope it affects, and what operational consequence follows**. +The exam objective groups Spark, domain, OneLake, and Apache Airflow settings under one heading. This chapter covers all four. The reliable way to reason through them is to identify the administrative scope, the workload or data affected, and the operational consequence before selecting an option. -## Start with the boundary +## Objective coverage - -A workspace is a collaboration and security boundary, but not every behavior is controlled at workspace scope. Some defaults inherit from capacity or tenant administration. Before changing a setting, identify the governing scope and whether the workspace administrator can override it. + +| Measured objective | Coverage in this chapter | +| --- | --- | +| Configure Spark workspace settings | Workspace defaults, environment overrides, runtimes, pools, and compute sizing | +| Configure domain workspace settings | Domain assignment, roles, discovery, delegated governance, and default domains | +| Configure OneLake workspace settings | Diagnostics, immutability, default storage tier, and lifecycle policies | +| Configure Apache Airflow workspace settings | Default runtime, starter and custom pools, sizing, autoscaling, and operating constraints | -| Setting family | Primary concern | Exam question to ask | +## Orient yourself: OneLake, workspace, item + + +[OneLake](https://learn.microsoft.com/en-us/fabric/onelake/onelake-overview) is the single organizational data lake supplied with a Fabric tenant. Workspaces divide that lake into independently administered project or domain containers. A OneLake workspace setting therefore controls storage or diagnostics behavior for that workspace; it does not create another OneLake. + +Keep three scopes separate: + +| Scope | Typical owner | Examples | +| --- | --- | --- | +| Tenant | Fabric administrator | Whether a capability is allowed or delegated across the organization | +| Workspace | Workspace administrator | OneLake diagnostics, default storage tier, and lifecycle policy | +| Item or data | Item owner or appropriately privileged user | Lakehouse permissions and OneLake security roles | + +Changing a workspace setting is not the same as granting access to data. Workspace roles and OneLake security roles belong to the security model; storage-tier and diagnostic settings govern how workspace data is retained, observed, or billed. + +## Configure Spark workspace settings + + +Fabric supplies managed Spark compute for Data Engineering and Data Science. At workspace scope, the administrator establishes the default compute behavior and decides whether individual environment items may override it. That governance switch is the first exam distinction: + +| Workspace choice | Result | +| --- | --- | +| Item-level customization disabled | Environment compute controls are unavailable; Spark jobs use the workspace default pool | +| Item-level customization enabled | Members and contributors can select session-level compute settings inside an environment | + +In the portal, the path is **Workspace settings > Data Engineering/Science > Spark settings**. The workspace administrator can select or create a pool and control whether item-level customization is allowed. An environment can then define a runtime, pool, driver sizing, and executor sizing when the workspace policy permits it. + +### Separate the Spark configuration layers + + +| Layer | Examples | When it applies | | --- | --- | --- | -| Spark | Compute defaults and runtime behavior | Does this change the workspace default or only one session? | -| Domain | Business ownership and discovery | Is the workspace assigned to the correct data domain? | -| OneLake | How data is exposed and accessed | Does the change affect access, discoverability, or data movement? | -| Apache Airflow | Managed orchestration configuration | Who owns connections, schedules, and operational monitoring? | +| Workspace default | Default pool and permission to customize item compute | Baseline for Spark jobs in the workspace | +| Environment item | Runtime, pool, driver cores/memory, executor cores/memory | Reusable configuration for attached notebooks and Spark job definitions | +| Spark application | Properties set through `spark.conf.set` | Application behavior inside a Spark session, not the environment's compute allocation | + +An environment change is not active merely because it was edited. Runtime and library changes must be saved and published. Publishing can fail when the selected runtime is incompatible with configured libraries or compute settings. + +### Choose a Spark pool and size compute + + +- Use the workspace default when workloads have similar requirements and centralized consistency matters most. +- Allow environment overrides when distinct workloads need different runtimes or driver/executor resources. +- Choose a starter pool when fast startup and managed defaults fit the workload. +- Choose an approved custom pool when node family, node size, autoscale, or dynamic allocation requirements justify dedicated configuration. +- Size the driver for coordination and planning needs; size executors for distributed processing. More resources are not automatically better if they reduce concurrency or waste capacity. + +When an exam question describes every notebook inheriting an unsuitable configuration, start at the workspace default. When only one workload needs different compute, look for an environment-level override—provided the workspace administrator has enabled customization. + +## Configure domain workspace settings + + +A Fabric domain is a logical business grouping used for organization, discovery, and federated governance. Assigning a workspace to a domain associates every item in that workspace with the domain and adds domain metadata that users can filter in the OneLake catalog. + +Domain assignment does **not** grant access to the workspace or its items. Visibility and data access still depend on workspace roles, item permissions, and data security. This is a frequent exam trap: domain membership organizes and governs; it is not an authorization grant. + +### Know who can perform each domain action + + +| Role | Relevant responsibility | +| --- | --- | +| Fabric administrator | Creates and deletes domains, appoints domain administrators and contributors, and can associate workspaces | +| Domain administrator | Manages the assigned domain, its contributors, workspace associations, presentation, and delegated settings | +| Domain contributor | Assigns a workspace they administer to an authorized domain or changes that assignment | +| Workspace administrator | Must administer the workspace before acting as its domain contributor | + +Fabric and domain administrators can assign workspaces centrally. A domain contributor works from the workspace settings and must also hold the workspace Admin role. Subdomains refine the logical hierarchy but inherit the administrators of their parent domain. + +### Default domains and delegated governance + + +A default domain targets specified users or security groups. Existing unassigned workspaces administered by those principals are assigned to the default domain, and new workspaces they create are assigned automatically. Existing domain assignments are preserved rather than overwritten by the default-domain mechanism. + +Some tenant settings can be delegated to domain administration. That enables a business area to apply domain-specific governance within boundaries established at tenant scope. Delegation does not transfer every Fabric-administrator capability to the domain administrator. + +Use these clues in scenario questions: + +- **Improve catalog discovery by business area:** assign workspaces to domains. +- **Automatically organize new workspaces for a department:** configure a default domain for the department's users or groups. +- **Let business owners manage an allowed governance setting:** delegate the supported tenant setting to domain scope. +- **Restrict access to domain data:** use workspace, item, or OneLake security—not domain assignment alone. + +## Terminology that drives the decision + + +| Term | Working meaning | Why it matters | +| --- | --- | --- | +| Diagnostic destination | A lakehouse that receives OneLake access events as JSON logs | It must satisfy placement and permission prerequisites before diagnostics can be enabled | +| Immutability period | A retention window during which diagnostic files cannot be modified or deleted | It protects evidence, but it also prevents early cleanup | +| Default storage tier | The tier used when a file has no explicitly assigned tier | Changing it can move affected files and generate transaction or retrieval charges | +| Lifecycle policy | The workspace's collection of automated tiering rules | A workspace has one policy, whose rules can target the workspace or path prefixes | +| Access-time tracking | Metadata needed for rules based on the last time a file was accessed | OneLake enables it when a lifecycle rule requires last-access conditions | + +## What the OneLake settings control + + +| Requirement in a scenario | Setting to examine | Important consequence | +| --- | --- | --- | +| Investigate who accessed data, when, and through which route | OneLake diagnostics | Events begin flowing to the chosen lakehouse after enablement; allow for activation latency | +| Protect diagnostic evidence from alteration for a defined period | Diagnostic-log immutability | Files cannot be changed or deleted until their retention period expires | +| Choose the initial tier for files without an explicit tier | Workspace default storage tier | Hot favors access; cooler tiers trade lower storage cost for higher access and transaction costs | +| Move inactive files automatically | Lifecycle management policy | Rules evaluate creation, modification, or access age and run asynchronously | + +The settings are related but not interchangeable. Diagnostics produces evidence. Immutability protects that evidence. The default tier supplies a baseline for untiered files. Lifecycle rules change tiers later when their conditions are met. + +## Responsibility and prerequisite boundaries + + +A workspace administrator manages these OneLake settings. For diagnostics, the administrator also needs contributor access to the destination lakehouse. Microsoft Learn states that the destination lakehouse must be in the same capacity as the workspaces being monitored. Network protection can narrow the valid destination further. + +Before enabling diagnostics, verify: + +1. A suitable lakehouse exists for the diagnostic events. +2. Capacity and network placement meet the documented constraints. +3. The configuring principal has both workspace-admin and destination-lakehouse permissions. +4. Retention, privacy, and cleanup ownership are agreed before immutability is applied. + +That last check is deliberately operational. Immutability is not a temporary display option: protected files remain noneditable and nondeletable until their individual retention windows expire. + +## Choose storage behavior deliberately + + +Hot, cool, and cold tiers exchange storage cost for access cost and minimum-retention commitments. The [OneLake storage-tier documentation](https://learn.microsoft.com/en-us/fabric/onelake/onelake-storage-tiers) identifies a 30-day minimum for cool storage and a 90-day minimum for cold storage. Moving or deleting data before the applicable minimum can incur an early-deletion charge. + +Use this sequence: + +1. Measure how often the data is read and changed. +2. Separate active paths from retention-oriented paths. +3. Estimate storage savings together with retrieval, transaction, and early-deletion costs. +4. Set a default tier only when it is appropriate for files that do not carry an explicit tier. +5. Use path-scoped lifecycle rules when different parts of the workspace have different access patterns. + +Do not select the coldest tier merely because it has the lowest storage rate. A frequently queried dataset can cost more overall and perform less predictably when retrieval behavior is ignored. + +## Build a lifecycle policy from the evidence + + +A workspace has one lifecycle policy containing rules. A rule combines a scope, enabled state, time condition, and tiering action. Conditions can use age since creation, modification, or last access. Rules without a path filter affect all eligible files in the workspace, so a broad rule deserves explicit review. + +Example decision—not a production prescription: + +```json +{ + "rules": [ + { + "name": "cool-stable-diagnostic-exports", + "enabled": true, + "scope": "Files/DiagnosticExports/", + "condition": "daysAfterModificationGreaterThan: 30", + "action": "tierToCool" + } + ] +} +``` + +The example expresses intent in a readable form. When implementing a policy, use the current portal or API schema from Microsoft Learn rather than treating this study representation as an importable payload. + +Lifecycle changes are asynchronous. Microsoft Learn notes that new rules can take up to 24 hours to take effect and that policies attempt to run daily. An exam scenario that requires an immediate, one-time tier change is therefore different from a scenario asking for ongoing automated management. + +## Portal and API views describe the same workspace state + + +In the portal, lifecycle management is under **Workspace settings > OneLake > Lifecycle management**. The Fabric REST API can also retrieve workspace OneLake settings: + +```http +GET https://api.fabric.microsoft.com/v1/workspaces/{workspaceId}/onelake/settings +``` + +The caller must have the Admin workspace role and an appropriate delegated scope such as `OneLake.Read.All` or `OneLake.ReadWrite.All`. A response can report diagnostics status, diagnostic-log immutability, and lifecycle state, including the default tier. Reading settings does not itself enable or modify them. + +## Configure Apache Airflow workspace settings + + +Fabric Apache Airflow jobs use a workspace default runtime chosen under **Workspace settings > Data Factory > Apache Airflow Runtime Settings**. The main decision is whether to use the managed starter pool or create a custom pool. + +| Characteristic | Starter pool | Custom pool | +| --- | --- | --- | +| Intended fit | Development and workloads that benefit from managed defaults | Production or workloads that need explicit sizing and capacity behavior | +| Startup behavior | Designed for immediate availability | A stopped pool can take time to resume | +| Uptime behavior | Deprovisions after inactivity | Remains available until manually paused | +| Configuration | Fixed managed configuration | Compute-node size, extra nodes, and autoscaling can be configured | +| Operational burden | Lower | Higher; the team owns sizing and uptime decisions | + +If item-level compute customization is disabled, Airflow environments use the workspace starter pool. When a custom pool is justified, the workspace administrator chooses its name, compute-node size, autoscale behavior, and extra nodes. Extra nodes increase concurrent DAG worker capacity; they are a concurrency decision, not a substitute for fixing an inefficient DAG. + +### Translate workload evidence into Airflow settings + + +Use a starter pool when the workload is intermittent, development-oriented, and tolerant of the managed configuration. Consider a custom pool when production scheduling needs predictable availability, nondefault sizing, autoscaling, or more concurrent workers. + +A useful decision sequence is: -## A practical decision sequence +1. Measure concurrent DAG runs, task duration, and scheduling delay. +2. Separate startup-delay problems from insufficient worker-concurrency problems. +3. Choose node size for task requirements and extra nodes for justified concurrency. +4. Enable autoscaling when demand varies enough to outweigh the added operational complexity. +5. Define who pauses, resumes, monitors, and revises the pool. - -1. Identify the required outcome: performance, governance, access, or orchestration. -2. Locate the narrowest administrative scope that owns the behavior. -3. Check inheritance and override rules before changing a default. -4. Validate the effect with a representative workload rather than assuming the setting is isolated. -5. Record the operational owner and a rollback path. +Current Microsoft Learn documentation states that Fabric Apache Airflow jobs do not support private networks or virtual networks. Treat that as a time-sensitive product limitation and recheck the authoritative source when designing a secured deployment. -## Exam distinction +## Exam distinctions - -Do not treat all workspace settings as interchangeable toggles. Spark settings primarily influence compute behavior; domain assignment communicates business organization; OneLake settings influence the shared data plane; and Apache Airflow settings support managed workflow orchestration. A scenario usually gives clues about **scope**, **owner**, and **effect**. + +- **Diagnostics versus monitoring:** diagnostics records OneLake data-access events. It is not a replacement for every workload-specific execution or performance log. +- **Immutability versus retention cleanup:** immutability prevents change during a fixed window; a separate cleanup process is still needed after the window expires. +- **Default tier versus lifecycle rule:** the default applies when no explicit tier is set; a rule evaluates conditions and changes eligible files over time. +- **Workspace setting versus data permission:** OneLake storage settings do not grant a user access to a lakehouse, table, row, or folder. +- **Workspace admin versus Fabric admin:** the workspace administrator manages the workspace policy; tenant-wide enablement and delegation remain tenant concerns. +- **Spark workspace default versus environment compute:** the workspace establishes the baseline and whether overrides are allowed; an environment holds reusable workload-specific configuration. +- **Environment compute versus `spark.conf`:** driver and executor resources belong to environment compute; `spark.conf` controls application properties inside a session. +- **Domain assignment versus authorization:** a domain organizes metadata and delegated governance; it does not grant access to workspace items or data. +- **Default domain versus forced reassignment:** a default domain assigns eligible unassigned and newly created workspaces; it preserves an existing assignment. +- **Airflow starter versus custom pool:** the starter pool minimizes administration; a custom pool adds control over sizing, autoscaling, concurrency, and uptime. ## Active recall -- Which settings would you examine first when every notebook in a workspace starts with an unsuitable Spark default? -- Why is assigning a workspace to a domain different from granting access to the workspace? -- What evidence would you collect before changing a setting that may affect multiple workloads? +1. Every Spark job in a workspace inherits an unsuitable pool. Which configuration layer should you examine first? +2. One notebook needs a different runtime and executor size. What must the workspace administrator allow before an environment can solve the problem? +3. Why is changing `spark.conf` not equivalent to changing environment compute? +4. A sales workspace should appear under Sales in the OneLake catalog. Which feature applies, and why does it not grant sales users access? +5. How does a default domain treat a workspace that already belongs to another domain? +6. A compliance team needs access events retained in a tamper-resistant form for 180 days. Which two OneLake capabilities work together, and what cleanup decision remains? +7. A workspace contains frequently queried curated tables and rarely accessed historical exports. Why is one workspace-wide default tier insufficient as the complete design? +8. What permissions and placement constraints must be checked before selecting a diagnostic lakehouse? +9. A question asks for a tier change to happen immediately. Why should you hesitate before choosing lifecycle management? +10. An Airflow team reports slow scheduling during concurrent production DAG runs. Which evidence distinguishes a node-sizing problem from a worker-concurrency problem? -## Source +## Sources and provenance - -Use the [official DP-700 study guide](https://learn.microsoft.com/en-us/credentials/certifications/resources/study-guides/dp-700) as the authoritative skills outline. This chapter is original study-oriented writing and will be expanded with setting-specific Microsoft Learn sources in the content-authoring milestone. + +This original study chapter was verified against the official Microsoft Learn pages linked below, retrieved September 1, 2026 (Pacific time). The Git-backed source registry records the retrieval timestamp and SHA-256 content hash for each page. diff --git a/docs/content-model.md b/docs/content-model.md index 9ec24f6..0b2d73b 100644 --- a/docs/content-model.md +++ b/docs/content-model.md @@ -29,18 +29,57 @@ MUST NOT infer order from identifiers or filenames. ## Sources and ownership Every chapter MUST map to at least one source declared in its book manifest. -The manifest records the official blueprint URL and effective date. Additional -chapter-specific sources will record retrieval dates and hashes in the refresh -milestone. +The manifest records the official blueprint URL and effective date. Each source +record contains its canonical Microsoft Learn URL, retrieval timestamp, SHA-256 +content hash, and direct chapter mappings. Source-to-chapter and +chapter-to-source mappings MUST agree so that a future refresh can identify the +affected chapters without a lineage service. + +Only `https://learn.microsoft.com` sources are accepted by the published +content contract. Raw retrieval responses are working data and MUST NOT be +committed as public content. The later refresh milestone will add bounded +retrieval, normalization, conditional requests, and readable change summaries; +the registry deliberately does not attempt to generalize those mappings into a +dependency graph. Microsoft Learn remains authoritative for Microsoft exam objectives and product behavior. Markdown chapters MUST contain original study-oriented writing and MUST NOT reproduce Microsoft Learn pages wholesale. +## Authoring depth + +The official study guide is the coverage contract, not the finished teaching +material. Every measured objective in a published chapter MUST receive +substantive coverage. A title match or repeated bullet is insufficient. + +For each objective, authors SHOULD provide the elements that materially aid +understanding: + +- an orientation and conceptual model; +- precise terminology and responsibility boundaries; +- decision criteria, trade-offs, and common failure modes; +- a worked configuration, query, transformation, or scenario when applicable; +- exam distinctions that separate easily confused features; +- active-recall questions; and +- exact authoritative links. + +Microsoft Learn is authoritative for Microsoft Fabric behavior. Chapters MAY +link to primary specifications or official upstream projects for underlying +concepts such as SQL, Apache Spark, Apache Airflow, Delta Lake, or Apache Kafka. +Those links supplement rather than override Microsoft Learn for Fabric-specific +claims. + +For example, coverage of Dataflow Gen2 MUST explain the transformations a +learner is expected to reason about—such as data types, filtering, joins, +grouping, shaping, schema handling, and query folding—along with destinations +and tool-selection trade-offs. Merely listing “Dataflow Gen2” does not satisfy +the objective. + ## Validation -Pydantic rejects duplicate identifiers, duplicate slugs, broken source -references, invalid paths, and unknown manifest fields. Content tests confirm -that every planned chapter file exists and maps at least one objective and one -source. Rendering tests verify sanitized HTML, stable anchors, external-link -behavior, and a reviewed golden fixture. +Pydantic rejects duplicate identifiers, duplicate slugs, non-Learn source +hosts, malformed hashes, one-way source mappings, broken source references, +invalid paths, and unknown manifest fields. Content tests confirm that every +planned chapter file exists and maps at least one objective and one source. +Rendering tests verify sanitized HTML, stable anchors, external-link behavior, +and a reviewed golden fixture. diff --git a/src/study_reader/content/models.py b/src/study_reader/content/models.py index 1c5faf5..ff977be 100644 --- a/src/study_reader/content/models.py +++ b/src/study_reader/content/models.py @@ -1,6 +1,6 @@ """Exam-agnostic, validated book content models.""" -from datetime import date +from datetime import date, datetime from functools import cached_property from typing import Self @@ -19,6 +19,9 @@ class Source(ContentModel): id: str = Field(pattern=r"^[a-z0-9][a-z0-9-]*$") title: str = Field(min_length=1) url: HttpUrl + retrieved_at: datetime + content_sha256: str = Field(pattern=r"^[a-f0-9]{64}$") + chapter_ids: tuple[str, ...] = Field(min_length=1) class Objective(ContentModel): @@ -110,12 +113,38 @@ def validate_references_and_identifiers(self) -> Self: if len(objective_ids) != len(set(objective_ids)): raise ValueError("objective identifiers must be unique") - source_ids = {source.id for source in self.sources} + source_ids = [source.id for source in self.sources] + if len(source_ids) != len(set(source_ids)): + raise ValueError("source identifiers must be unique") + + chapter_id_set = set(chapter_ids) + for source in self.sources: + if source.url.host != "learn.microsoft.com": + raise ValueError(f"source {source.id} is not hosted on Microsoft Learn") + unknown_chapters = set(source.chapter_ids) - chapter_id_set + if unknown_chapters: + raise ValueError( + f"source {source.id} maps unknown chapters: " + f"{sorted(unknown_chapters)}" + ) + + source_id_set = set(source_ids) for chapter in self.chapters: - unknown_sources = set(chapter.source_ids) - source_ids + unknown_sources = set(chapter.source_ids) - source_id_set if unknown_sources: raise ValueError( f"chapter {chapter.id} references unknown source: " f"{sorted(unknown_sources)}" ) + + for source in self.sources: + referencing_chapters = { + chapter.id + for chapter in self.chapters + if source.id in chapter.source_ids + } + if referencing_chapters != set(source.chapter_ids): + raise ValueError( + f"source {source.id} chapter mappings must be bidirectional" + ) return self diff --git a/src/study_reader/reader/static/reader.css b/src/study_reader/reader/static/reader.css index 691f532..2f448c3 100644 --- a/src/study_reader/reader/static/reader.css +++ b/src/study_reader/reader/static/reader.css @@ -182,7 +182,8 @@ a:focus-visible, summary:focus-visible { outline: 3px solid #d69c52; outline-off .source-card { display: grid; grid-template-columns: 1fr auto; gap: 1.5rem; align-items: center; margin-top: 4rem; padding: 1.5rem; border-radius: .8rem; background: var(--accent-soft); } .source-card h2 { margin: 0; font: 600 1.35rem Georgia, serif; } .source-card p:not(.eyebrow) { max-width: 46ch; margin: .4rem 0 0; color: var(--muted); font-size: .88rem; line-height: 1.5; } -.source-card > a { padding: .75rem 1rem; border: 1px solid var(--accent); border-radius: .5rem; font-size: .8rem; font-weight: 700; text-decoration: none; } +.source-links { display: grid; gap: .45rem; min-width: min(100%, 18rem); } +.source-links a { padding: .6rem .75rem; border: 1px solid var(--accent); border-radius: .5rem; font-size: .75rem; font-weight: 700; line-height: 1.3; text-decoration: none; } .complete-button { display: flex; @@ -264,7 +265,7 @@ a:focus-visible, summary:focus-visible { outline: 3px solid #d69c52; outline-off .chapter-prose { font-size: 1.04rem; line-height: 1.72; } .chapter-prose h2 { margin-top: 2.5rem; } .source-card { grid-template-columns: 1fr; } - .source-card > a { text-align: center; } + .source-links { width: 100%; } .chapter-navigation { grid-template-columns: 1fr; } .chapter-navigation .next { grid-column: 1; text-align: left; } } diff --git a/src/study_reader/reader/templates/chapter.html b/src/study_reader/reader/templates/chapter.html index 393c93f..f92c309 100644 --- a/src/study_reader/reader/templates/chapter.html +++ b/src/study_reader/reader/templates/chapter.html @@ -143,11 +143,13 @@
This reader summarizes and teaches. Microsoft Learn remains the source of truth.
- {% for source in sources %} - - Open official DP-700 study guide - - {% endfor %} +