Skip to content

Project profiles: port from Quarto 1 (bd-fu16z22k) - #492

Merged
cscheid merged 8 commits into
mainfrom
feature/bd-fu16z22k-project-profiles
Aug 10, 2026
Merged

Project profiles: port from Quarto 1 (bd-fu16z22k)#492
cscheid merged 8 commits into
mainfrom
feature/bd-fu16z22k-project-profiles

Conversation

@cscheid

@cscheid cscheid commented Aug 10, 2026

Copy link
Copy Markdown
Member

Summary

Ports Quarto 1's project profiles to Q2: activation via --profile / QUARTO_PROFILE, profile.default + profile.group, config overlays (_quarto-<name>.yml), local overrides (_quarto.yml.local), profile environment files (_environment-<name>), and conditional content (when-/unless- × format/profile/meta) — built greenfield as an AST transform. Where Q1 guesses silently, Q2 validates with span-carrying diagnostics.

Plan (design decisions, divergence table, per-phase records): claude-notes/plans/2026-08-10-project-profiles-port.md. Strand: bd-fu16z22k.

One phase per commit

Commit Phase
ce502820 Phase 0 — activation resolution core (project_profile.rs, catalog codes Q-5-19..22)
117364cf Phase 1 — _quarto-<name>.yml overlays + _quarto.yml.local, FileId/span discipline at every bind_config_source site
5da7208e Phase 2 — --profile on render/get-config/publish, pass-1 cache-key inputs, subprocess QUARTO_PROFILE, -v echo
6e941e2e Phase 3 — _environment-<profile> loading + dotenv QUARTO_PROFILE bootstrap (fills the seam PR #486 left)
e107ff20 Phase 4 — conditional content trio (new Q-2-42 diagnostics)
4e2eecfa Phase 5 — docs pages, cross-runner smoke fixture, deferred-work strands

(9e0dc551 is a small process-docs change decided mid-session: commit-and-continue at clean phase boundaries.)

Design highlights

  • Activation precedence (Q1 parity): --profile replaces QUARTO_PROFILE, which beats the _environment{,.local} bootstrap, which beats profile.default in _quarto.yml.local, then _quarto.yml; group expansion appends each group's first member after explicit selections. First-listed profile wins merge conflicts (consistent with the _environment-<p> layer order).
  • No process-env mutation (policy from Load project _environment files (bd-environment-files-372u9qbs) #486): the resolved set travels as data on ProjectConfig.active_config_profiles; child processes (engines, render scripts) get the normalized list via Command::env unconditionally — the one deliberate exception to the real-env-wins rule — and {{< env QUARTO_PROFILE >}} special-cases it.
  • Terminology guard: "profile" already means DocumentProfile (pass-1 summary) in q2; the new code consistently says project profiles / active_config_profiles, and the pass-1 cache key gained the project-profile inputs (PROFILE_KEY_VERSION → 2) so a profile switch can't serve stale pass-1 results.
  • Strictness upgrades over Q1 (all silent there): Q-5-19 unknown active profile, Q-5-20 malformed profile: shape, Q-5-21 invalid names ([A-Za-z0-9][A-Za-z0-9._-]*, empty selections), Q-5-22 inert profile: keys, Q-2-42 condition-attribute typos. Behavioral divergences (array concat instead of union-dedup; comma-OR in condition values) are documented in the docs page's "Differences from Quarto 1".
  • Deferred with strands: preview --profile flag threading (bd-pfgc273f; QUARTO_PROFILE=x q2 preview works today), metadata-files decision (bd-spb7mobo), Lua quarto.project.profile (bd-ip1lrgra), preview watch (bd-kzwt3xcu), gitignore scaffolding (bd-47hhbmaj).

Test plan

TDD red-first throughout; ~90 new tests: 41 resolution unit tests, 25 overlay integration tests, 35 binary-driven CLI/e2e tests (comma/repeated flags, precedence, diagnostics through the binary, env-file layering via {{< env >}} in rendered HTML, crossref renumbering under hidden floats), 11 conditional-content walker tests, cache-key domain-separation tests, a real-spawn script-env test, and a smoke-all fixture activated via profile.default so the native and WASM runners both exercise the stack.

E2E verified with real invocations (recorded in the plan): overlay title in rendered HTML, q2 get-config divergence with/without --profile, -v provenance echo, and a live jupyter kernel printing QUARTO_PROFILE=advanced,production.

Gates: cargo nextest run --workspace green (11,397 tests), full cargo xtask verify (WASM leg included) green, clippy/fmt/cargo xtask lint clean.

Incidental fixes: verbose_to_filter never matched the q2 bin crate's tracing targets; the tracing fmt layer wrote to stdout instead of stderr.

🤖 Generated with Claude Code

@cscheid cscheid added the feature-port PR produced by the feature-porting workflow (Q1 to Q2 port) label Aug 10, 2026
@posit-snyk-bot

posit-snyk-bot commented Aug 10, 2026

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

cscheid and others added 7 commits August 10, 2026 13:14
Pure-logic module crates/quarto-core/src/project/project_profile.rs:
QUARTO_PROFILE-string parsing (Q1 [ ,]+ split; edge-trim + dedup fix
Q1's empty-name bug), strict name validation
([A-Za-z0-9][A-Za-z0-9._-]*), site-aware `profile:` extraction with
stripping (BaseConfig / LocalConfig / Overlay), and the activation
precedence chain (CLI > env > env-file > local default > base
default, then profile.group first-member expansion appended after
explicit selections).

New error-catalog codes: Q-5-19 (unknown profile — emitted in Phase
1), Q-5-20 (invalid profile: shape), Q-5-21 (invalid profile name /
empty selection), Q-5-22 (inert profile: key).

TDD: 41 unit tests written first and observed failing on stubs.
Full workspace suite green (11262 tests).

Plan: claude-notes/plans/2026-08-10-project-profiles-port.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
During user-approved plan execution, commit at each clean phase
boundary (checklist passed, workspace tests green) without stopping
for approval; approval still required for ad-hoc commits, dirty
states, and always for pushing. Decided in the bd-fu16z22k session.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ProjectContext::parse_config now resolves the project-profile
activation set (explicit selection via new discover_with_profile, or
QUARTO_PROFILE via runtime.env_get) and merges `_quarto-<name>.yml`
overlays plus `_quarto.yml.local` into the project metadata before
any field extraction — project.type, output-dir, render globs,
resources, render scripts, and brand are all profile-aware. Overlays
merge in reverse activation order (first-listed profile wins, Q1
parity); `_quarto.yml.local` is the highest-priority layer and also
contributes profile.default.

Diagnostics: Q-5-19 warning for an explicitly-selected profile that
matches nothing; Q-5-22 for inert profile: keys in overlays /
group-in-local; Q-5-20/21 shape and name errors abort discovery with
spans. New ProjectConfig fields active_config_profiles (with
provenance) and profile_config_paths, registered as bind_config_source
candidates at every site (MetadataMergeStage, render/publish/preview
script contexts, project resources, theme error candidates) so
overlay-anchored diagnostics render real spans.

TDD: 25 integration tests (project_profile_overlays.rs) written first
and observed failing against a delegating stub. Full workspace suite
green (11287 tests).

Also this session: killed 2338 accumulated orphaned Jupyter kernels +
4932 stale connection files on the dev machine and filed bd-hxhnnlzs —
a bare `cargo nextest run --workspace` reproducibly leaks ~15 orphan
ipykernel processes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
--profile now works end-to-end on q2 render, get-config, and publish
(threaded as data through every discover, including the
post-pre-render-script and publish render-time re-discoveries; empty
vec = flag absent, non-empty replaces QUARTO_PROFILE — Q1 parity).
Preview keeps env-var activation only for now; the flag form needs
HubContext threading (bd-pfgc273f). Project-less discovery also
resolves and validates the selection, so bad names abort even
without a _quarto.yml.

Pass-1 cache key: active profile names (count-prefixed,
order-sensitive) + overlay/local file bytes join Pass1KeyInputs;
PROFILE_KEY_VERSION bumped to 2. A profile switch or overlay edit
can no longer serve stale DocumentProfiles.

Child processes see the normalized, group-expanded QUARTO_PROFILE
unconditionally (the one exception to the real-env-wins rule from
PR #486): engine subprocesses via EngineExecutionStage's env pairs,
render scripts via RenderScriptsContext.quarto_profile, and
{{< env QUARTO_PROFILE >}} via an EnvShortcodeHandler special case
that beats the real environment.

-v now echoes the active set with provenance. Two latent logging
gaps fixed en route: verbose_to_filter directives never matched the
q2 bin crate's tracing targets (quarto= does not prefix-match q2=),
and the tracing fmt layer wrote to stdout instead of stderr.

TDD: 14 binary-driven CLI tests written first (12 observed failing),
plus cache-key, spawn-env, and shortcode unit tests. Full workspace
green (11365 tests). E2E verified and recorded in the plan: overlay
title in rendered HTML, env shortcode resolving "prod,draft",
get-config divergence, -v echo.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fills the seam PR #486 left: _environment-<name> files now load for
each active project profile (first-listed profile wins, matching the
overlay rule), threaded from ProjectConfig.active_config_profiles
through StageContext::new and subprocess_env_for_project.

Q1's dotenv bootstrap is ported: QUARTO_PROFILE defined in
_environment.local / _environment (never in profile variants — no
activation recursion) selects profiles when neither --profile nor
the real environment variable does, and loses to both.

TDD: 6 binary-driven tests (3 observed failing first) covering
bootstrap precedence, env-file layering through {{< env >}} in
rendered HTML, and the no-recursion rule. Full workspace green
(11371 tests). Closes bd-ev8mk1rp.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Greenfield port of Q1's content-hidden.lua as an AST transform:
.content-visible / .content-hidden on divs, spans, and code blocks
with when-format / unless-format, when-profile / unless-profile,
and when-meta / unless-meta. Condition kinds AND together;
comma/space-separated values within one condition OR (q2 extension —
Q1's repeated-attribute OR is unrepresentable in q2's map-shaped
Attr); unless-* negates; bare .content-hidden always hides;
survivors keep classes but lose condition attributes.

Runs FIRST in the Normalization phase, so hidden content disappears
before callout assembly, shortcode resolution (no spurious warnings
from excluded content), and crossref numbering (a hidden float
consumes no number — tested). when-format reuses pampa's
quarto.doc.is_format alias table (made pub) against the canonical
lua format, so attributes and Lua filters can never disagree.
when-meta does dotted-path lookup in the merged metadata with Q1
truthiness, which lets profile overlays drive content via config.

Strictness: new Q-2-42 warning for unknown when-*/unless-*
spellings and for elements carrying both marker classes (hidden
wins); Q1 is silent on both.

TDD: 15 binary-driven tests written first (14 observed failing) +
11 unit tests on the walker. Full workspace green (11397 tests).
E2E verified both ways and recorded in the plan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New user-facing docs page guides/projects/profiles.qmd (activation,
overlay merging, profile.default/group, conditional content, code
visibility, and a "Differences from Quarto 1" section);
environment.qmd gains _environment-<profile> documentation (file
table, precedence, secrets callout, no-recursion rule). Docs site
rendered with q2 and inspected.

smoke-all fixture metadata/project-profiles/: activation via
profile.default so all three runners exercise overlays +
when-profile + when-meta with no CLI flags — including the WASM
runner (the feature's WASM-path coverage). Full cargo xtask verify
(WASM leg included) green.

Also verified with a real jupyter kernel: a python cell sees
QUARTO_PROFILE=advanced,production under --profile.

Strand bookkeeping: bd-mlj6 closed as implemented; deferred work
filed as bd-pfgc273f (preview flag), bd-ip1lrgra (Lua
quarto.project.profile), bd-kzwt3xcu (preview watch), bd-47hhbmaj
(gitignore scaffolding), bd-spb7mobo (metadata-files decision).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cscheid
cscheid force-pushed the feature/bd-fu16z22k-project-profiles branch from 4e2eecf to 1d8262e Compare August 10, 2026 18:20
…h arg

A test added on main after this branch diverged calls
ShortcodeResolveTransform::with_lua_support with the pre-profiles
6-argument signature; pass quarto_profile: None like every other
test caller. (Semantic merge conflict caught by PR CI.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cscheid
cscheid force-pushed the feature/bd-fu16z22k-project-profiles branch from 1d8262e to bd4301c Compare August 10, 2026 18:20
@cscheid
cscheid merged commit 7306498 into main Aug 10, 2026
8 checks passed
@cscheid
cscheid deleted the feature/bd-fu16z22k-project-profiles branch August 10, 2026 18:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature-port PR produced by the feature-porting workflow (Q1 to Q2 port)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants