Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/dot-project-spec-hash.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
d8ca8361c0aff434e9d7288851717f88f149785419ca062a520cdd506ae6b27e
860df23ecfd970b3d603098b6597a787e7ee6954b8592cdd17e431198eff70b4
2 changes: 1 addition & 1 deletion .specify/feature.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"feature_directory": "specs/029-openai-parity-adapter"}
{"feature_directory": "specs/030-dot-project-spec-sync"}
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -381,5 +381,5 @@ else:
<!-- SPECKIT START -->
For additional context about technologies to be used, project structure,
shell commands, and other important information, read the current plan:
[`specs/029-openai-parity-adapter/plan.md`](specs/029-openai-parity-adapter/plan.md)
[`specs/030-dot-project-spec-sync/plan.md`](specs/030-dot-project-spec-sync/plan.md)
<!-- SPECKIT END -->
73 changes: 66 additions & 7 deletions packages/darnit/src/darnit/context/dot_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
following the CNCF .project/ specification.

Specification: https://github.com/cncf/automation/tree/main/utilities/dot-project
Targeted Spec Version: 1.1.0 (based on types.go as of 2026-02)
Targeted Spec Version: 1.2.0 (based on types.go at commit 641b80619cd5, 2026-06-29)

The reader is tolerant of unknown fields for forward compatibility with
spec evolution. Required fields are validated per the CNCF types.go struct.
Expand All @@ -22,21 +22,40 @@
# Write updates (preserving comments)
writer = DotProjectWriter("/path/to/repo")
writer.update({"security": {"policy": {"path": "SECURITY.md"}}})

Reconciliation history
----------------------
- 1.1.0 -> 1.2.0 (feature 030-dot-project-spec-sync, 2026-08-15):
* `project_lead`: accepts scalar or list per upstream `StringOrSlice`;
collapses to the first non-empty string element for the existing
scalar attribute on `ProjectConfig`.
* `package_managers[*]`: accepts scalar or list per upstream
`StringOrSlice`; collapses to the first non-empty string element per
registry key.
* `cncf_slack_channel`: deprecated upstream; darnit still populates the
existing scalar attribute from the old YAML key and emits
`DeprecationWarning` naming the replacement `slack_channels` and the
version (1.2.0) that carries the alias. Alias will be removed in the
release immediately following 1.2.0 (feature 030 Q2).
* `slack_channels`: new upstream field. Parsed via the existing
unknown-field catch-all into `ProjectConfig._extra["slack_channels"]`.
Not projected onto any `ProjectConfig` attribute (feature 030 Q1:
parse-only scope).
"""

from __future__ import annotations

import logging
import warnings
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

logger = logging.getLogger(__name__)

# Targeted .project/ spec version
# Based on cncf/automation types.go
# Update this when we verify compatibility with newer spec versions
DOT_PROJECT_SPEC_VERSION = "1.1.0"
# Targeted .project/ spec version. Bumped 1:1 with the tracked-hash file
# in `.github/dot-project-spec-hash.txt` per feature 030 Q3.
DOT_PROJECT_SPEC_VERSION = "1.2.0"
DOT_PROJECT_SPEC_URL = "https://github.com/cncf/automation/tree/main/utilities/dot-project"


Expand Down Expand Up @@ -515,6 +534,30 @@ def _normalize_handle(self, handle: str) -> str:
"""Normalize a maintainer handle (strip @ and whitespace)."""
return handle.strip().lstrip("@")

@staticmethod
def _coerce_scalar_or_list(value: Any) -> str:
"""Coerce the CNCF `StringOrSlice` YAML shape to a single scalar string.

Feature 030 (parse-only): upstream `types.go` introduced a
`StringOrSlice` helper allowing `project_lead` and each
`package_managers[*]` value to be either a plain string or a list of
strings. Darnit still exposes the scalar-shape attribute on
`ProjectConfig`; multi-value support is a separate feature. This
helper returns the input verbatim when it's a scalar, the first
element when it's a non-empty list of strings, and ``""`` for None
or an empty list. Any other shape yields ``""``.
"""
if value is None:
return ""
if isinstance(value, str):
return value
if isinstance(value, list):
for item in value:
if isinstance(item, str) and item:
return item
return ""
return ""

def _parse_config(self, data: dict[str, Any]) -> ProjectConfig:
"""Parse raw YAML data into ProjectConfig."""
config = ProjectConfig()
Expand Down Expand Up @@ -551,14 +594,30 @@ def _parse_config(self, data: dict[str, Any]) -> ProjectConfig:
config.schema_version = data.get("schema_version", "")
config.type = data.get("type", "")
config.slug = data.get("slug", "")
config.project_lead = data.get("project_lead", "")
config.project_lead = self._coerce_scalar_or_list(data.get("project_lead", ""))
if "cncf_slack_channel" in data:
warnings.warn(
(
"The .project/ specification field `cncf_slack_channel` is "
"deprecated upstream. This alias is accepted by darnit "
"spec version 1.2.0 and will be removed in the next release. "
"Migrate to the `slack_channels` list form defined in the "
"CNCF spec: "
"https://github.com/cncf/automation/tree/main/utilities/dot-project"
),
DeprecationWarning,
stacklevel=2,
)
config.cncf_slack_channel = data.get("cncf_slack_channel", "")
config.website = data.get("website", "")
config.artwork = data.get("artwork", "")
config.repositories = data.get("repositories", [])
config.mailing_lists = data.get("mailing_lists", [])
config.social = data.get("social", {})
config.package_managers = data.get("package_managers", {})
config.package_managers = {
registry: self._coerce_scalar_or_list(value)
for registry, value in (data.get("package_managers") or {}).items()
}

# Parse adopters file reference
if "adopters" in data:
Expand Down
36 changes: 36 additions & 0 deletions specs/030-dot-project-spec-sync/checklists/requirements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Specification Quality Checklist: Sync `.project/` reader with current CNCF spec

**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-08-14
**Feature**: [spec.md](../spec.md)

## Content Quality

- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed

## Requirement Completeness

- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified

## Feature Readiness

- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification

## Notes

- Content Quality: The spec avoids naming languages or frameworks in requirements. It does name the upstream artifact (`types.go`) and the darnit reader (`dot_project.py`) because those are entities being reconciled and not stack choices; a stakeholder ignoring implementation details still needs to know which files are the reconciliation surface.
- Requirement Completeness: No clarification markers were introduced. Two potentially ambiguous points (whether to expose newly added upstream fields; whether to keep old field-name aliases when upstream renames) are handled in Assumptions and Edge Cases rather than as open questions, because reasonable maintenance defaults exist.
- Success Criteria: All five criteria are technology-agnostic. SC-002 references "a fixture" as a *verification method* rather than an implementation detail; the fixture-vs-live-repo distinction is a testing choice, not a system choice.
90 changes: 90 additions & 0 deletions specs/030-dot-project-spec-sync/contracts/reader-contract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Reader Contract: `dot_project` module

## Scope

This contract enumerates every public callable, dataclass attribute, module constant, and warning behavior of `packages/darnit/src/darnit/context/dot_project.py` after the current feature lands. It exists so that the next reconciliation can diff this file against its own contract and immediately see what a downstream consumer might notice.

The reader is a library-internal contract, not an HTTP or RPC surface; "public" means "what darnit's own code and its test suite call into." No stability guarantee is offered to third-party importers.

## Module constants

| Name | Pre-reconciliation | Post-reconciliation | Change class |
|------|--------------------|---------------------|--------------|
| `DOT_PROJECT_SPEC_VERSION` | `"1.1.0"` | `"1.2.0"` | BUMP (per Q3: 1:1 with tracked-hash file) |
| `DOT_PROJECT_SPEC_URL` | `"https://github.com/cncf/automation/tree/main/utilities/dot-project"` | (unchanged) | KEPT |

## Public callables

### `DotProjectReader.load(project_dir: Path) -> ProjectConfig | None`

Signature: **unchanged**.

Behavior deltas:
- Accepts `.project/project.yaml` files that use either scalar or list form for `project_lead` and `package_managers[*]` (previously only scalar was accepted; list would fail YAML-to-dataclass coercion). List form collapses to first element for both.
- On encountering the YAML key `cncf_slack_channel`, emits `warnings.warn(msg, DeprecationWarning, stacklevel=2)` where `msg` names both the old key (`cncf_slack_channel`), the recommended migration (`slack_channels`), and the release in which the alias will be removed. Value still populates `ProjectConfig.cncf_slack_channel`.
- On encountering the YAML key `slack_channels`, silently records the raw parsed value under `ProjectConfig._extra["slack_channels"]`. Not exposed via a `ProjectConfig` attribute.

Return type: **unchanged** (`ProjectConfig | None`).

### `DotProjectReader.parse(...)` and other public methods

Signatures: **unchanged**.

Behavior deltas: same three as `load()` above, since they all funnel through the same parsing helpers.

### `DotProjectWriter.*`

Signatures: **unchanged**. Write path is out of scope for this reconciliation; the reader-side reshape is one-way (writer continues to serialize `project_lead` and `package_managers[*]` as scalars, matching how darnit had authored them pre-reconciliation).

## Public dataclass attributes

See [data-model.md](../data-model.md) for the complete per-field table. Only the following attributes have any post-reconciliation behavior change; all other attributes are `KEPT` verbatim:

| Attribute | Type | Change class | Consumer impact |
|-----------|------|--------------|-----------------|
| `ProjectConfig.project_lead` | `str` | KEPT-WITH-RESHAPE | Accepts a list-form YAML input; consumer reads the first element only. |
| `ProjectConfig.cncf_slack_channel` | `str` | KEPT-WITH-ALIAS | Populated from the old YAML key with a deprecation warning; not populated from the new `slack_channels` key. |
| `ProjectConfig.package_managers` | `dict[str, str]` | KEPT-WITH-RESHAPE | Accepts per-key list-form values; consumer reads the first element per key. |

Consumer impact is bounded to "receives the same value type it did before, possibly a different content when the source YAML used the new list form." No consumer sees a new attribute type or a missing attribute.

## Warning behavior

### `cncf_slack_channel` deprecation

**Trigger**: Presence of the YAML key `cncf_slack_channel` in a `.project/project.yaml` being parsed.

**Channel**: `warnings.warn(message, DeprecationWarning, stacklevel=2)`.

**Exact message text** (subject to review at implementation time):

```
The .project/ specification field `cncf_slack_channel` is deprecated
upstream. This alias is accepted by darnit v0.1.x (spec version 1.2.0)
and will be removed in the next release. Migrate to the `slack_channels`
list form defined in the CNCF spec:
https://github.com/cncf/automation/tree/main/utilities/dot-project
```

The message intentionally names darnit's version identifier (`1.2.0`) so a maintainer grepping a warning traceback can identify which reconciliation introduced the alias.

## Backward compatibility guarantees

For every `.project/project.yaml` file that parses successfully under the pre-reconciliation reader (spec version `1.1.0`), the post-reconciliation reader (spec version `1.2.0`) MUST:

1. Also parse the file successfully (no new hard failures).
2. Produce a `ProjectConfig` whose attribute values equal the pre-reconciliation values for every attribute in the [data-model.md](../data-model.md) table, EXCEPT that a `cncf_slack_channel`-carrying file MAY additionally emit a `DeprecationWarning`.
3. Produce a `_extra` dict that includes any newly-seen upstream keys (specifically `slack_channels` when the file has been updated to use it).

Item (2) is the mechanical property SC-002 hangs on, and the fixture-plus-golden-dict test at `tests/darnit/context/test_full_field_coverage.py` (introduced by this feature) is what checks it.

## Forward compatibility surface

The reader's `_extra: dict[str, Any]` catch-all is the forward-compatibility mechanism. Every future upstream drift that only ADDS fields will land in `_extra` and require no code change. Future drifts that RENAME or RESHAPE fields will require a new reconciliation feature; the reader does NOT attempt to speculatively handle unseen renames.

## Non-goals

- The reader does NOT expose `project_leads: list[str]` as a new attribute (Q1: parse-only).
- The reader does NOT expose `slack_channels: list[SlackChannel]` as a new attribute (Q1: parse-only).
- The reader does NOT round-trip the new list form on write; `DotProjectWriter` continues to emit scalars for `project_lead` and `package_managers[*]`.
- The reader does NOT bump its version identifier past `1.2.0` in this reconciliation; the next reconciliation bumps again per Q3.
Loading
Loading