feat(state-hub): v0.3 registration workflow + ingest-sbom + CLAUDE.md template update
- scripts/ingest_sbom.py: lockfile parser + API poster for uv.lock, requirements.txt,
package-lock.json, yarn.lock, Cargo.lock; auto-detects from repo root
- Makefile: make ingest-sbom REPO=<slug> [LOCKFILE=<path>] target
- scripts/register_project.sh: adds {REPO_SLUG} template substitution + optional
SBOM ingest prompt at end of registration (non-fatal if venv not ready)
- scripts/project_claude_md.template: adds Contribution Tracking + SBOM sections
documenting register_contribution(), update_contribution_status(), ingest-sbom,
and the contrib/ directory layout
- workplans/CUST-WP-0002: all 15 tasks → done, status → completed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -73,6 +73,11 @@ list-repos:
|
|||||||
@test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN is required."; exit 1)
|
@test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN is required."; exit 1)
|
||||||
curl -sf "http://127.0.0.1:8000/repos/?domain=$(DOMAIN)" | python3 -m json.tool
|
curl -sf "http://127.0.0.1:8000/repos/?domain=$(DOMAIN)" | python3 -m json.tool
|
||||||
|
|
||||||
|
## Ingest a repo's lockfile into the SBOM store: make ingest-sbom REPO=the-custodian [LOCKFILE=uv.lock]
|
||||||
|
ingest-sbom:
|
||||||
|
@test -n "$(REPO)" || (echo "ERROR: REPO is required. Usage: make ingest-sbom REPO=<slug> [LOCKFILE=<path>]"; exit 1)
|
||||||
|
uv run python scripts/ingest_sbom.py --repo "$(REPO)" $(if $(LOCKFILE),--lockfile "$(LOCKFILE)",)
|
||||||
|
|
||||||
## Check a repo for ADR-001 compliance: make validate-adr REPO=/path/to/repo [DOMAIN=custodian]
|
## Check a repo for ADR-001 compliance: make validate-adr REPO=/path/to/repo [DOMAIN=custodian]
|
||||||
validate-adr:
|
validate-adr:
|
||||||
@test -n "$(REPO)" || (echo "ERROR: REPO is required. Usage: make validate-adr REPO=<path> [DOMAIN=<slug>]"; exit 1)
|
@test -n "$(REPO)" || (echo "ERROR: REPO is required. Usage: make validate-adr REPO=<path> [DOMAIN=<slug>]"; exit 1)
|
||||||
|
|||||||
276
state-hub/scripts/ingest_sbom.py
Normal file
276
state-hub/scripts/ingest_sbom.py
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Ingest a repo's lockfile into the State Hub SBOM store.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python ingest_sbom.py --repo <slug> [--lockfile <path>] [--api-base <url>]
|
||||||
|
|
||||||
|
Auto-detects lockfile type:
|
||||||
|
uv.lock → Python ecosystem
|
||||||
|
requirements.txt → Python ecosystem (basic)
|
||||||
|
package-lock.json → Node ecosystem
|
||||||
|
yarn.lock → Node ecosystem
|
||||||
|
Cargo.lock → Rust ecosystem
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
API_BASE = os.environ.get("API_BASE", "http://127.0.0.1:8000").rstrip("/")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Lockfile parsers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _parse_uv_lock(path: Path) -> list[dict]:
|
||||||
|
"""Parse uv.lock TOML format (v0.1 — [[package]] blocks)."""
|
||||||
|
entries = []
|
||||||
|
current: dict | None = None
|
||||||
|
|
||||||
|
for line in path.read_text().splitlines():
|
||||||
|
stripped = line.strip()
|
||||||
|
if stripped == "[[package]]":
|
||||||
|
if current:
|
||||||
|
entries.append(current)
|
||||||
|
current = {}
|
||||||
|
elif current is not None:
|
||||||
|
if stripped.startswith("name = "):
|
||||||
|
current["package_name"] = stripped.split("=", 1)[1].strip().strip('"')
|
||||||
|
elif stripped.startswith("version = "):
|
||||||
|
current["package_version"] = stripped.split("=", 1)[1].strip().strip('"')
|
||||||
|
|
||||||
|
if current:
|
||||||
|
entries.append(current)
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"package_name": e.get("package_name", "unknown"),
|
||||||
|
"package_version": e.get("package_version"),
|
||||||
|
"ecosystem": "python",
|
||||||
|
"license_spdx": None,
|
||||||
|
"is_direct": False, # uv.lock doesn't distinguish; treat all as transitive
|
||||||
|
"is_dev": False,
|
||||||
|
}
|
||||||
|
for e in entries
|
||||||
|
if "package_name" in e
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_requirements_txt(path: Path) -> list[dict]:
|
||||||
|
"""Parse requirements.txt (basic — name==version lines)."""
|
||||||
|
entries = []
|
||||||
|
for line in path.read_text().splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith("#") or line.startswith("-"):
|
||||||
|
continue
|
||||||
|
# Handle: pkg==1.2.3, pkg>=1.2, pkg
|
||||||
|
m = re.match(r"^([A-Za-z0-9_.\-]+)(?:[>=<!~^]+([^\s;]+))?", line)
|
||||||
|
if m:
|
||||||
|
entries.append({
|
||||||
|
"package_name": m.group(1),
|
||||||
|
"package_version": m.group(2),
|
||||||
|
"ecosystem": "python",
|
||||||
|
"license_spdx": None,
|
||||||
|
"is_direct": True,
|
||||||
|
"is_dev": False,
|
||||||
|
})
|
||||||
|
return entries
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_package_lock_json(path: Path) -> list[dict]:
|
||||||
|
"""Parse package-lock.json (npm) — packages dict."""
|
||||||
|
try:
|
||||||
|
data = json.loads(path.read_text())
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
print(f"Warning: cannot parse {path}: {e}", file=sys.stderr)
|
||||||
|
return []
|
||||||
|
|
||||||
|
packages = data.get("packages", {})
|
||||||
|
entries = []
|
||||||
|
for pkg_path, info in packages.items():
|
||||||
|
if not pkg_path: # root package
|
||||||
|
continue
|
||||||
|
name = info.get("name") or pkg_path.split("node_modules/")[-1]
|
||||||
|
entries.append({
|
||||||
|
"package_name": name,
|
||||||
|
"package_version": info.get("version"),
|
||||||
|
"ecosystem": "node",
|
||||||
|
"license_spdx": info.get("license"),
|
||||||
|
"is_direct": not info.get("indirect", False),
|
||||||
|
"is_dev": bool(info.get("dev", False)),
|
||||||
|
})
|
||||||
|
return entries
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_yarn_lock(path: Path) -> list[dict]:
|
||||||
|
"""Parse yarn.lock — basic name extraction."""
|
||||||
|
entries = []
|
||||||
|
current_names: list[str] = []
|
||||||
|
current_version: str | None = None
|
||||||
|
|
||||||
|
for line in path.read_text().splitlines():
|
||||||
|
stripped = line.strip()
|
||||||
|
if not stripped or stripped.startswith("#"):
|
||||||
|
continue
|
||||||
|
if not line.startswith(" ") and stripped.endswith(":"):
|
||||||
|
# New package block header: "name@version::" or "\"name@version\":"
|
||||||
|
# May list multiple versions: "name@^1.0, name@~1.0:"
|
||||||
|
current_names = []
|
||||||
|
current_version = None
|
||||||
|
for part in stripped.rstrip(":").split(","):
|
||||||
|
m = re.match(r'"?([^@"]+)@', part.strip())
|
||||||
|
if m:
|
||||||
|
current_names.append(m.group(1).strip())
|
||||||
|
elif stripped.startswith("version "):
|
||||||
|
current_version = stripped.split('"')[1] if '"' in stripped else None
|
||||||
|
elif not stripped and current_names and current_version:
|
||||||
|
for name in current_names:
|
||||||
|
entries.append({
|
||||||
|
"package_name": name,
|
||||||
|
"package_version": current_version,
|
||||||
|
"ecosystem": "node",
|
||||||
|
"license_spdx": None,
|
||||||
|
"is_direct": False,
|
||||||
|
"is_dev": False,
|
||||||
|
})
|
||||||
|
current_names = []
|
||||||
|
current_version = None
|
||||||
|
|
||||||
|
return entries
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_cargo_lock(path: Path) -> list[dict]:
|
||||||
|
"""Parse Cargo.lock TOML format ([[package]] blocks)."""
|
||||||
|
entries = []
|
||||||
|
current: dict | None = None
|
||||||
|
|
||||||
|
for line in path.read_text().splitlines():
|
||||||
|
stripped = line.strip()
|
||||||
|
if stripped == "[[package]]":
|
||||||
|
if current:
|
||||||
|
entries.append(current)
|
||||||
|
current = {}
|
||||||
|
elif current is not None:
|
||||||
|
if stripped.startswith("name = "):
|
||||||
|
current["package_name"] = stripped.split("=", 1)[1].strip().strip('"')
|
||||||
|
elif stripped.startswith("version = "):
|
||||||
|
current["package_version"] = stripped.split("=", 1)[1].strip().strip('"')
|
||||||
|
|
||||||
|
if current:
|
||||||
|
entries.append(current)
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"package_name": e.get("package_name", "unknown"),
|
||||||
|
"package_version": e.get("package_version"),
|
||||||
|
"ecosystem": "rust",
|
||||||
|
"license_spdx": None,
|
||||||
|
"is_direct": False,
|
||||||
|
"is_dev": False,
|
||||||
|
}
|
||||||
|
for e in entries
|
||||||
|
if "package_name" in e
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
_LOCKFILE_PARSERS = {
|
||||||
|
"uv.lock": _parse_uv_lock,
|
||||||
|
"requirements.txt": _parse_requirements_txt,
|
||||||
|
"package-lock.json": _parse_package_lock_json,
|
||||||
|
"yarn.lock": _parse_yarn_lock,
|
||||||
|
"Cargo.lock": _parse_cargo_lock,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def detect_lockfile(repo_path: Path) -> tuple[Path, str] | None:
|
||||||
|
"""Return (lockfile_path, ecosystem) for the first recognised lockfile found."""
|
||||||
|
for name in _LOCKFILE_PARSERS:
|
||||||
|
candidate = repo_path / name
|
||||||
|
if candidate.exists():
|
||||||
|
return candidate, name
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_lockfile(lockfile_path: Path) -> list[dict]:
|
||||||
|
filename = lockfile_path.name
|
||||||
|
parser = _LOCKFILE_PARSERS.get(filename)
|
||||||
|
if parser is None:
|
||||||
|
print(f"Error: unsupported lockfile type '{filename}'", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return parser(lockfile_path)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# API submission
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def post_ingest(api_base: str, repo_slug: str, entries: list[dict]) -> dict:
|
||||||
|
payload = json.dumps({"repo_slug": repo_slug, "entries": entries}).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
f"{api_base}/sbom/ingest/",
|
||||||
|
data=payload,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||||
|
return json.loads(resp.read())
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
body = e.read().decode(errors="replace")
|
||||||
|
print(f"HTTP {e.code} from API: {body}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
except urllib.error.URLError as e:
|
||||||
|
print(f"API unreachable: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Entry point
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description="Ingest a lockfile into the State Hub SBOM store.")
|
||||||
|
parser.add_argument("--repo", required=True, help="Managed-repo slug (e.g. 'the-custodian')")
|
||||||
|
parser.add_argument("--lockfile", help="Path to lockfile (auto-detected if omitted)")
|
||||||
|
parser.add_argument("--repo-path", default=".", help="Repo root for auto-detection (default: cwd)")
|
||||||
|
parser.add_argument("--api-base", default=API_BASE, help="State Hub API base URL")
|
||||||
|
parser.add_argument("--dry-run", action="store_true", help="Parse only — do not submit")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.lockfile:
|
||||||
|
lockfile_path = Path(args.lockfile).resolve()
|
||||||
|
else:
|
||||||
|
found = detect_lockfile(Path(args.repo_path).resolve())
|
||||||
|
if not found:
|
||||||
|
print(
|
||||||
|
f"No recognised lockfile found in '{args.repo_path}'. "
|
||||||
|
"Supported: " + ", ".join(_LOCKFILE_PARSERS),
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
lockfile_path, _ = found
|
||||||
|
print(f"Auto-detected: {lockfile_path}")
|
||||||
|
|
||||||
|
entries = parse_lockfile(lockfile_path)
|
||||||
|
print(f"Parsed {len(entries)} packages from {lockfile_path.name}")
|
||||||
|
|
||||||
|
if args.dry_run:
|
||||||
|
print(json.dumps(entries[:5], indent=2))
|
||||||
|
if len(entries) > 5:
|
||||||
|
print(f" … and {len(entries) - 5} more")
|
||||||
|
return
|
||||||
|
|
||||||
|
result = post_ingest(args.api_base, args.repo, entries)
|
||||||
|
print(f"Ingested {result.get('ingested', '?')} entries for repo '{args.repo}'")
|
||||||
|
print(f"Snapshot at: {result.get('snapshot_at', '?')}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -82,6 +82,63 @@ add_progress_event(
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Contribution Tracking
|
||||||
|
|
||||||
|
This project tracks upstream contributions in `contrib/` — bug reports, feature
|
||||||
|
requests, extension-point proposals, and upstream PRs — as canonical Markdown files.
|
||||||
|
|
||||||
|
**Directory layout:**
|
||||||
|
```
|
||||||
|
contrib/
|
||||||
|
bug-reports/ # br-YYYY-MM-DD--org--repo--slug.md
|
||||||
|
feature-requests/ # fr-YYYY-MM-DD--org--repo--slug.md
|
||||||
|
extension-points/ # EP-{DOMAIN}-NNN--org--repo--slug.md
|
||||||
|
upstream-prs/ # upr-YYYY-MM-DD--org--repo--slug.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Templates: `~/the-custodian/canon/standards/contrib-templates/`
|
||||||
|
Convention: `~/the-custodian/canon/standards/contribution-convention_v0.1.md`
|
||||||
|
|
||||||
|
**Register a contribution in the State Hub:**
|
||||||
|
```
|
||||||
|
register_contribution(
|
||||||
|
type="upr", # br | fr | ep | upr
|
||||||
|
title="Add injectTocTop to Observable Framework",
|
||||||
|
target_org="observablehq",
|
||||||
|
target_repo="framework",
|
||||||
|
body_path="contrib/upstream-prs/2026-02-26--observablehq--framework--inject.md",
|
||||||
|
related_workstream_id="<uuid>",
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Update status when upstream responds:**
|
||||||
|
```
|
||||||
|
update_contribution_status(contribution_id="<uuid>", status="submitted")
|
||||||
|
# then: acknowledged → accepted → merged
|
||||||
|
```
|
||||||
|
|
||||||
|
**List all contributions for this domain:**
|
||||||
|
```
|
||||||
|
get_contributions(target_repo="framework")
|
||||||
|
```
|
||||||
|
|
||||||
|
### SBOM
|
||||||
|
|
||||||
|
Software Bill of Materials for this repo is tracked in the State Hub.
|
||||||
|
|
||||||
|
**Ingest the current lockfile:**
|
||||||
|
```bash
|
||||||
|
cd ~/the-custodian/state-hub
|
||||||
|
make ingest-sbom REPO={REPO_SLUG}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Check licence risk:**
|
||||||
|
```
|
||||||
|
get_licence_report()
|
||||||
|
```
|
||||||
|
|
||||||
|
**View SBOM dashboard:** `http://localhost:3000/sbom`
|
||||||
|
|
||||||
### Quick Reference
|
### Quick Reference
|
||||||
|
|
||||||
See `~/the-custodian/state-hub/mcp_server/TOOLS.md` for a compact tool reference.
|
See `~/the-custodian/state-hub/mcp_server/TOOLS.md` for a compact tool reference.
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ else
|
|||||||
-e "s|{PROJECT_NAME}|$PROJECT_NAME|g" \
|
-e "s|{PROJECT_NAME}|$PROJECT_NAME|g" \
|
||||||
-e "s|{DOMAIN}|$DOMAIN|g" \
|
-e "s|{DOMAIN}|$DOMAIN|g" \
|
||||||
-e "s|{TOPIC_ID}|$TOPIC_ID|g" \
|
-e "s|{TOPIC_ID}|$TOPIC_ID|g" \
|
||||||
|
-e "s|{REPO_SLUG}|$REPO_SLUG|g" \
|
||||||
"$TEMPLATE" > "$CLAUDE_MD"
|
"$TEMPLATE" > "$CLAUDE_MD"
|
||||||
echo " Written."
|
echo " Written."
|
||||||
fi
|
fi
|
||||||
@@ -186,3 +187,21 @@ echo " Repo slug: $REPO_SLUG"
|
|||||||
echo " CLAUDE.md: $CLAUDE_MD"
|
echo " CLAUDE.md: $CLAUDE_MD"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Next: restart Claude Code for the MCP server to be available in this project."
|
echo "Next: restart Claude Code for the MCP server to be available in this project."
|
||||||
|
|
||||||
|
# ── Optional: SBOM ingest ─────────────────────────────────────────────────────
|
||||||
|
if [[ "$ADDITIONAL" != "--additional" ]]; then
|
||||||
|
echo ""
|
||||||
|
read -r -p "==> Run SBOM ingest now? (auto-detects lockfile in $PROJECT_PATH) [y/N] " INGEST_NOW
|
||||||
|
if [[ "$INGEST_NOW" =~ ^[Yy]$ ]]; then
|
||||||
|
echo "==> Ingesting SBOM for '$REPO_SLUG' ..."
|
||||||
|
INGEST_UV="$STATE_HUB_DIR/.venv/bin/python"
|
||||||
|
if [[ -x "$INGEST_UV" ]]; then
|
||||||
|
"$INGEST_UV" "$SCRIPT_DIR/ingest_sbom.py" \
|
||||||
|
--repo "$REPO_SLUG" \
|
||||||
|
--repo-path "$PROJECT_PATH" \
|
||||||
|
--api-base "$API_BASE" && echo " SBOM ingested." || echo " SBOM ingest failed (non-fatal)."
|
||||||
|
else
|
||||||
|
echo " Skipping: .venv not found. Run 'make install' first, then 'make ingest-sbom REPO=$REPO_SLUG'."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ id: CUST-WP-0002
|
|||||||
type: workplan
|
type: workplan
|
||||||
title: "State Hub v0.3 — Contribution Tracking & SBOM"
|
title: "State Hub v0.3 — Contribution Tracking & SBOM"
|
||||||
domain: custodian
|
domain: custodian
|
||||||
status: active
|
status: completed
|
||||||
owner: custodian
|
owner: custodian
|
||||||
topic_slug: custodian
|
topic_slug: custodian
|
||||||
state_hub_workstream_id: 2446400d-6d01-4679-a314-92af0601c608
|
state_hub_workstream_id: 2446400d-6d01-4679-a314-92af0601c608
|
||||||
@@ -47,7 +47,7 @@ Three interconnected layers:
|
|||||||
```task
|
```task
|
||||||
id: CUST-WP-0002-T01
|
id: CUST-WP-0002-T01
|
||||||
state_hub_task_id: c2d7df02-5f37-4c02-a418-fe7ab0050edc
|
state_hub_task_id: c2d7df02-5f37-4c02-a418-fe7ab0050edc
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ relationship to state-hub. Authoritative reference for other repos.
|
|||||||
```task
|
```task
|
||||||
id: CUST-WP-0002-T02
|
id: CUST-WP-0002-T02
|
||||||
state_hub_task_id: 5ef89639-950c-4d57-9578-6d0211edc2df
|
state_hub_task_id: 5ef89639-950c-4d57-9578-6d0211edc2df
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ prose skeleton.
|
|||||||
```task
|
```task
|
||||||
id: CUST-WP-0002-T03
|
id: CUST-WP-0002-T03
|
||||||
state_hub_task_id: e8251873-647c-4a4b-b283-298260b19c22
|
state_hub_task_id: e8251873-647c-4a4b-b283-298260b19c22
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ Status: draft.
|
|||||||
```task
|
```task
|
||||||
id: CUST-WP-0002-T04
|
id: CUST-WP-0002-T04
|
||||||
state_hub_task_id: 1c982457-de52-4fc1-a439-3bf3120bb5b6
|
state_hub_task_id: 1c982457-de52-4fc1-a439-3bf3120bb5b6
|
||||||
status: todo
|
status: done
|
||||||
priority: low
|
priority: low
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ proper instance of the custodian master spec, not a parallel one.
|
|||||||
```task
|
```task
|
||||||
id: CUST-WP-0002-T05
|
id: CUST-WP-0002-T05
|
||||||
state_hub_task_id: c41d71bd-dc88-4201-bd9a-3f8a4eae4910
|
state_hub_task_id: c41d71bd-dc88-4201-bd9a-3f8a4eae4910
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ created_at, updated_at. Alembic migration.
|
|||||||
```task
|
```task
|
||||||
id: CUST-WP-0002-T06
|
id: CUST-WP-0002-T06
|
||||||
state_hub_task_id: 28c9bd38-05d8-4466-bff3-3ba4e3957635
|
state_hub_task_id: 28c9bd38-05d8-4466-bff3-3ba4e3957635
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -142,7 +142,7 @@ Prerequisite: v0.5 P2.1 (managed_repos table) must be complete.
|
|||||||
```task
|
```task
|
||||||
id: CUST-WP-0002-T07
|
id: CUST-WP-0002-T07
|
||||||
state_hub_task_id: ee1cd17c-6dbd-4321-bb72-4499147c4837
|
state_hub_task_id: ee1cd17c-6dbd-4321-bb72-4499147c4837
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -157,7 +157,7 @@ delete (sets status=withdrawn). Schemas: `ContributionCreate`,
|
|||||||
```task
|
```task
|
||||||
id: CUST-WP-0002-T08
|
id: CUST-WP-0002-T08
|
||||||
state_hub_task_id: 25f7ab5c-69d8-41d7-a108-38380eb1f3a9
|
state_hub_task_id: 25f7ab5c-69d8-41d7-a108-38380eb1f3a9
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -180,7 +180,7 @@ be complete.
|
|||||||
```task
|
```task
|
||||||
id: CUST-WP-0002-T09
|
id: CUST-WP-0002-T09
|
||||||
state_hub_task_id: edcf9b02-8177-4abc-b133-d303cc7ea19d
|
state_hub_task_id: edcf9b02-8177-4abc-b133-d303cc7ea19d
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -196,7 +196,7 @@ normalised list; POSTs to `/sbom/ingest/`. Makefile target:
|
|||||||
```task
|
```task
|
||||||
id: CUST-WP-0002-T10
|
id: CUST-WP-0002-T10
|
||||||
state_hub_task_id: 51b2999b-a9c7-4871-8ef8-f5c927ac2454
|
state_hub_task_id: 51b2999b-a9c7-4871-8ef8-f5c927ac2454
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -212,7 +212,7 @@ priority: high
|
|||||||
```task
|
```task
|
||||||
id: CUST-WP-0002-T11
|
id: CUST-WP-0002-T11
|
||||||
state_hub_task_id: 20c5c2ae-7758-42cc-885c-a886957e44c2
|
state_hub_task_id: 20c5c2ae-7758-42cc-885c-a886957e44c2
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -232,7 +232,7 @@ must be complete.
|
|||||||
```task
|
```task
|
||||||
id: CUST-WP-0002-T12
|
id: CUST-WP-0002-T12
|
||||||
state_hub_task_id: 1eb13411-b2da-4d0d-8fe2-e926d029c50f
|
state_hub_task_id: 1eb13411-b2da-4d0d-8fe2-e926d029c50f
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -247,7 +247,7 @@ selector as decisions page). Apply `injectTocTop` for page-level KPI.
|
|||||||
```task
|
```task
|
||||||
id: CUST-WP-0002-T13
|
id: CUST-WP-0002-T13
|
||||||
state_hub_task_id: 1267f313-1bf3-4059-8276-bfefcd9f6aed
|
state_hub_task_id: 1267f313-1bf3-4059-8276-bfefcd9f6aed
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -262,7 +262,7 @@ drill-down accordion. Register in `observablehq.config.js`.
|
|||||||
```task
|
```task
|
||||||
id: CUST-WP-0002-T14
|
id: CUST-WP-0002-T14
|
||||||
state_hub_task_id: abab0022-d3a4-45ef-a7e1-e0e7c05d295f
|
state_hub_task_id: abab0022-d3a4-45ef-a7e1-e0e7c05d295f
|
||||||
status: todo
|
status: done
|
||||||
priority: low
|
priority: low
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -277,7 +277,7 @@ otherwise). Uses `/state/summary` fields added in P2.3 and P2.4.
|
|||||||
```task
|
```task
|
||||||
id: CUST-WP-0002-T15
|
id: CUST-WP-0002-T15
|
||||||
state_hub_task_id: 47987720-23db-4465-861b-1f93bb1bb391
|
state_hub_task_id: 47987720-23db-4465-861b-1f93bb1bb391
|
||||||
status: todo
|
status: done
|
||||||
priority: low
|
priority: low
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user