Skip to content
Open
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
37 changes: 37 additions & 0 deletions .github/workflows/cleanliness.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Scheduled repo-cleanliness run (the nine questions β€” brief: croadfeldt/dav
# docs/repo-cleanliness-review.md). PR gating is validate.yml; this is the monthly
# full-suite sweep + the semantic-review prompt, so cleanliness stays ACTIVE.
name: cleanliness
on:
schedule:
- cron: "17 7 1 * *"
workflow_dispatch:
permissions:
issues: write
contents: read
jobs:
cleanliness:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
- run: pip install jsonschema pyyaml
- name: gates
id: gates
run: |
set +e; fail=0; : > /tmp/summary.md
for c in tests/validate_contracts.py tests/check_terminology.py tests/check_estate_tokens.py tests/check_links.py; do
out=$(python3 "$c" 2>&1); rc=$?
[ $rc -ne 0 ] && fail=1
printf '### `%s` β€” %s\n```\n%s\n```\n' "$c" "$([ $rc -eq 0 ] && echo PASS || echo FAIL)" "$(echo "$out" | tail -8)" >> /tmp/summary.md
done
echo "fail=$fail" >> "$GITHUB_OUTPUT"
- name: open review issue
env: { GH_TOKEN: "${{ github.token }}" }
run: |
title="Cleanliness review $(date -u +%Y-%m)"
{ echo "Monthly repo-cleanliness run β€” deterministic gates $([ '${{ steps.gates.outputs.fail }}' = '1' ] && echo '**FAILED β€” fix before other work**' || echo 'all green').";
echo; cat /tmp/summary.md; echo;
echo "**Semantic sweep due (Q1–Q7 residues):** run the nine-question review brief β€” croadfeldt/dav \`docs/repo-cleanliness-review.md\` β€” with review agents over this repo (boundary/ADR-008 both directions, profile-ladder + vocab drift in prose, doc scoping); findings as file:line + severity into a cleanup plan."; } > /tmp/body.md
gh issue create --title "$title" --body-file /tmp/body.md
24 changes: 24 additions & 0 deletions .github/workflows/lint-openapi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: AEP OpenAPI lint

# ADR-AEP-001: lint DCM's OpenAPI specs against the AEP conventions (aep.dev)
# using the AEP Spectral ruleset. ADVISORY for now (continue-on-error) β€” findings
# are reported while the baseline is burned down (see schemas/openapi/AEP-CONFORMANCE.md);
# flip `continue-on-error` to false once the error baseline reaches zero.

on:
pull_request:
paths: [ "schemas/openapi/**", ".spectral.yaml", ".github/workflows/lint-openapi.yml" ]
workflow_dispatch: {}

jobs:
aep-lint:
runs-on: ubuntu-latest
continue-on-error: true # advisory until the baseline is cleared
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: "20" }
- name: Install AEP linter
run: npm install --no-audit --no-fund @stoplight/spectral-cli @aep_dev/aep-openapi-linter
- name: Lint OpenAPI specs
run: npx spectral lint "schemas/openapi/*.yaml" --ruleset .spectral.yaml --format github-actions || true
57 changes: 57 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Architecture validation gates for the DCM spec repo (docs + machine-readable contracts).
# Three independent gates so a failure names its own dimension:
# contracts β€” JSON Schema + OpenAPI parse/structure (tests/validate_contracts.py)
# terminology β€” locked naming decisions, 2026-06-30 (tests/check_terminology.py)
# estate-tokens β€” no homelab identifiers in the public spec (tests/check_estate_tokens.py)
name: validate
on:
pull_request:
push:
branches: [main]

jobs:
contracts:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install jsonschema pyyaml
# JSON Schemas valid + OpenAPI parse/structure (catches a broken contract merging).
- run: python3 tests/validate_contracts.py
# UC dimension vocabulary (DIM-001) β€” needs pyyaml, lives in this job
- run: python3 tests/check_uc_dimensions.py
# UC persona vocabulary (PER-001) β€” canonical persona set, single-sourced
- run: python3 tests/check_uc_personas.py

terminology:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
# Locked terminology (policy-type merge, Realized not Fulfilled, etc.). Pure stdlib.
- run: python3 tests/check_terminology.py

estate-tokens:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
# No homelab identifiers in the public spec (hashed denylist). Pure stdlib.
- run: python3 tests/check_estate_tokens.py

links:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
# Referential integrity (repo-cleanliness Q8 β€” croadfeldt/dav docs/repo-cleanliness-review.md):
# every relative markdown link resolves. Pure stdlib.
- run: python3 tests/check_links.py
58 changes: 58 additions & 0 deletions examples/dependency-resolution-walkthrough.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Example: resolving the dependency-modeling estate

A worked walkthrough of the resolution pass (`architecture/dependency-resolution.md`) over the
anonymized example estate (the UDLM repo's `examples/dependency-modeling/`). It shows how
dependencies authored several ways collapse into one effective graph and a derived order β€” and how
little the resolver actually has to *do*, because most inheritance is plain transitivity.

## The authored estate (15 resources)

- **Power (component chain).** `host-a` contains two power supplies: `psu-a1 β†’ feed-a` and
`psu-a2 β†’ feed-b` β€” two independent rails. `host-b` (at the bench) contains one: `psu-b1 β†’
feed-wall`. Power is authored on the PSU, not the host.
- **Bundling (a shared node).** `core-services` declares the shared platform dependencies once:
`depends_on idm` (DirectoryService) + `depends_on dns` (AddressService). `svc-app` and `svc-db`
each add a single `depends_on core-services`.
- **Scope.** `host-a` and `host-b` carry no identity edge β€” they share the realm via `tenant_uuid`.
- **App (direct edge).** `svc-app depends_on svc-db`.
- **Location.** `host-a` in `loc-rack`, `host-b` in `loc-bench`.

Each resource authored only its *local* facts β€” a PSU names its feed, a service names the shared node
it bundles through and the DB it talks to. Nobody hand-wired "host-a depends on feed-a and feed-b" or
"svc-app depends on idm and dns."

## Resolution (build-time, not stored)

1. **Seed** with the authored edges.
2. **Scope derivation** β€” the one real derivation: `tenant_uuid` is a field, not an edge, so the
resolver injects `host-a`/`host-b` β†’ the realm's `idm`/`dns` (cycle-safe; the realm's own upstreams
are excluded).
3. **Everything else is already there.** No bundle-expansion pass, no transitive-chain pass:
- `svc-app β†’ core-services β†’ {idm, dns}` β€” svc-app inherits idm+dns as **secondary dependencies**
by traversal alone. Depend on a node; get its deps. That is all "bundling" is.
- `host-a β†’ psu-a1 β†’ feed-a` and `host-a β†’ psu-a2 β†’ feed-b` β€” **host-a's power is the union of both
feeds**. A coarse "host-a on one UPS" edge would have dropped the second rail.

Effective graph = authored + the scope edges, 0 cycles.

## Derived shutdown order (topological)

| step | resources | note |
|---|---|---|
| 0 | psu-a1, psu-a2, psu-b1, svc-app | leaf consumers stop first |
| 1 | feed-a, feed-b, feed-wall, host-a, svc-db | |
| 2 | core-services, host-b, loc-rack | |
| 3 | **dns, idm**, loc-bench | control-plane gate β€” identity/DNS hold until last |

Reverse for startup. The payoff: `host-a` stops before **both** its feeds (redundancy honored),
`svc-app` before `core-services` before `idm`/`dns` (bundled secondary deps ordered correctly), and
identity/DNS last β€” with the resolver only having to derive the scope edges.

## Reproduce

```
python3 shutdown_order.py <path-to>/examples/dependency-modeling # from the estate-explorer tools
```

See `architecture/dependency-resolution.md` for the mechanics (including the anti-pattern note on why
there is no bundle type) and the UDLM repo's `docs/dependency-modeling.md` for the data-model side.
Loading
Loading