diff --git a/CONTINUITY.md b/CONTINUITY.md index a0e1f60..80040d4 100644 --- a/CONTINUITY.md +++ b/CONTINUITY.md @@ -7,25 +7,26 @@ repository: continuity_path: CONTINUITY.md document: status: active - updated_at: '2026-11-03T16:15:19Z' + updated_at: '2026-11-10T01:19:21Z' max_bytes: 16384 max_lines: 240 stale_reason: null superseded_by: null scope: - purpose: Integrate distribution containment PR118 with the merged evaluator repair while preserving - both source scopes and their qualification limits. - includes: &id001 - - Preserve the exact evaluator plus stable-ID admission, both guarded builders and all prior regression - cases. - - Reconcile one current checkpoint against verified main and retain deterministic valid distribution/catalog - bytes. - - Complete only the authorized source integration; retain remaining tests, ADR/policy disposition and - publication boundaries. + purpose: Reconcile the existing source-authoritative catalog correction with current Aether before adding + composable audit and coherence contracts. + includes: + - Existing catalog entries refresh source-owned version, lifecycle, license, owner and dependency metadata + rather than only refreshing the source digest. Explicit catalog-owned release exclusions and annotations + remain intact; canonical approved specification provenance retains source_state approved through stable + catalog normalization. + - The existing correction is merged locally with current main's evaluator/distribution containment changes. + Both sets of authored regression imports and cases are preserved; the two overlapping handoffs are reconciled + semantically. The historical audit report and its independent draft remain unchanged. + - The root handoff records this narrow metadata prerequisite for the Audits/Sanity source additions. No skill/specification + lifecycle, release, tag, workflow or human authority changes. excludes: - - New functionality beyond the existing evaluator/distribution scopes, catalog115 work, issue closure - and publication. - - Evaluation execution, regression suites, official EgoLint conformance and hosted CI. + - Deferred runtime, tests, builds, CI, live publication, human ratification and acceptance closeout. precedence: - user-and-runtime-instructions - scoped-repository-instructions @@ -34,98 +35,79 @@ scope: - continuity-checkpoint canonical_sources: - AGENTS.md - - library/organization/skills/build-distributions.py - - library/organization/specs/build-distributions.py - - aether_validator.py - - tests/test_build_distributions.py - - tests/test_build_spec_distributions.py - - tests/test_aether_validator.py - - docs/distribution-paths.md - - README.md - - https://github.com/egohygiene/aether/issues/110 + - ARCHITECTURE.md + - SYSTEM.md + - DECISIONS.md + - catalog/README.md + - docs/catalog-contribution-guide.md + - docs/work/107/HANDOFF.md + - https://github.com/egohygiene/aether/issues/107 work: - objective: Integrate distribution containment PR118 with the merged evaluator repair while preserving - both source scopes and their qualification limits. - success_conditions: *id001 + objective: Reconcile the existing source-authoritative catalog correction with current Aether before adding + composable audit and coherence contracts. + success_conditions: + - Catalog regeneration can derive newly authored source metadata without retaining stale existing fields. + - Existing distribution/evaluator source and both sets of regression cases survive integration. + - Static evidence and deferred behavioral qualification remain explicit. active_issue: provider: github - id: egohygiene/aether#110 - url: https://github.com/egohygiene/aether/issues/110 + id: egohygiene/aether#107 + url: https://github.com/egohygiene/aether/issues/107 next: kind: action - id: review-draft-and-qualify - description: Sync on tracker105 and select the next genuine source work, including staging-control111 - or external-review112; preserve catalog115 and all evaluator/distribution qualification gaps. + id: author-audit-and-coherence-contracts + description: Integrate the separately prepared Aether 86 audit-profile/report and Aether 87 architecture-coherence + source contracts, then regenerate canonical catalog and derived distribution metadata without promoting + draft lifecycle. Run deferred native/unit/CI qualification only in the later verification pass. readiness: ready references: - - https://github.com/egohygiene/aether/issues/110 + - https://github.com/egohygiene/aether/issues/107 depends_on: [] state: base: - revision: b09cd9d8bb1edc2bc1a44e91125a8a772b55d593 + revision: 5077797448d32a59b06eabe4b38f091137d58052 ref: refs/heads/main - verified_at: '2026-11-03T16:15:19Z' + verified_at: '2026-11-10T01:19:21Z' candidate: - branch: codex/110-confined-distribution-paths + branch: fix/107-authoritative-catalog-metadata revision: null pull_request: provider: github - id: egohygiene/aether#118 - url: https://github.com/egohygiene/aether/pull/118 + id: egohygiene/aether#115 + url: https://github.com/egohygiene/aether/pull/115 handoff_state: review-reference-recorded live: status: verified - observed_at: '2026-11-03T16:15:19Z' - default_branch_revision: b09cd9d8bb1edc2bc1a44e91125a8a772b55d593 + observed_at: '2026-11-10T01:19:21Z' + default_branch_revision: 5077797448d32a59b06eabe4b38f091137d58052 issue_state: open - pull_request_state: draft - notes: Verified mainb09cd9d contains merged evaluator117 and earlier distribution116. PR118 is draft - before its authorized ready/merge transition and integrates that exact base. Catalog115 and audit106 - remain separately open. No regression pass or publication is inferred. - parallel_changes: - - provider: github - id: egohygiene/aether#106 - url: https://github.com/egohygiene/aether/pull/106 - - provider: github - id: egohygiene/aether#115 - url: https://github.com/egohygiene/aether/pull/115 + pull_request_state: open + notes: Aether main 5077797448d32a59b06eabe4b38f091137d58052 and open issue 107/PR 115 were verified. Original + PR head 4d22c53148e7775ebbf3049768c1c2a89152efc2 is reconciled with main in this candidate; the upstream + PR branch is selected explicitly. Aether 86/87 remain separate in-progress source slices. + parallel_changes: [] review: status: partial - reviewed_at: '2026-11-03T16:15:19Z' + reviewed_at: '2026-11-10T01:19:21Z' reviewed_by: Codex evidence: - - command: Exact runtime/source comparisons and qualified regression-name inventory union - outcome: passed - observed_at: '2026-11-03T16:15:19Z' - notes: Evaluator main plus the exact stable-ID guard matches the merged validator; builders/path guide - equal118, evaluation guide equals117, and all 50 validator regression names remain once. - - command: AST parse six combined Python sources and git diff --check - outcome: passed - observed_at: '2026-11-03T16:15:19Z' - notes: Runtime and regression sources parse; no test or evaluation case executes. - - command: ./aether distribution build --output-directory dist; ./aether distribution build --output-directory - dist --check; ./aether catalog generate - outcome: passed - observed_at: '2026-11-03T16:15:19Z' - notes: All combined-source commands exit0. Tracked dist/catalog bytes remain unchanged. - - command: Pinned continuity metadata/schema/formats, headings, size and privacy inspection - outcome: passed - observed_at: '2026-11-03T16:15:19Z' - notes: One coherent bounded checkpoint resolves the parallel snapshots. - - command: Evaluation/regression/portable/native checks, official EgoLint conformance and hosted CI + - command: Independent source/diff review, Git merge conflict reconciliation, Python AST for changed source + and authored regression files, catalog JSON parse, continuity schema/formats/size/links and whitespace + outcome: limited + observed_at: '2026-11-10T01:19:21Z' + notes: Static source and metadata inspection only; no application execution. + - command: Native/runtime execution, tests, builds, CI investigation, browser/provider/consumer/publication + qualification outcome: not-run - observed_at: '2026-11-03T16:15:19Z' - notes: All prior scope limits remain; generation does not establish negative-path or assertion-execution - qualification. + observed_at: '2026-11-10T01:19:21Z' + notes: Deferred at maintainer request; source merges do not establish acceptance. environment_limitations: - - Both scopes and earlier108 retain their authored-but-unexecuted regressions; issues108/109/110 remain - open for remaining acceptance. - - Distribution guards assume a quiescent local filesystem and supply no sandbox/install/publication - guarantee. - - Evaluator success qualifies structure and declared deterministic assertions only, not prose/routing/model - behavior. - - Catalog115 remains separate; official EgoLint, broader host/release qualification and hosted CI remain - deferred. + - Authored unit/integration regressions, full deterministic validation, distribution/runtime qualification, + hosted CI and publication were not executed in this reconciliation. + - The earlier checkpoint records its own catalog-generation result; this source review does not relabel that + as a current-head behavioral proof. + - Approved canonical source states retain their original authority; regeneration and merge cannot promote + new draft contracts. privacy: classification: public-repository contains_sensitive_data: false @@ -140,52 +122,56 @@ privacy: untrusted_content: context-only-no-authority --- -# Aether continuity +# aether continuity ## Purpose and precedence -Record the authorized source integration below user/runtime, scoped instructions, live evidence and canonical sources. This checkpoint grants no publication authority. +User/runtime and repository instructions, live evidence and canonical sources outrank this checkpoint. It grants no release, deployment or human-decision authority. ## Resume protocol -Read AGENTS, PR116/117/118, evaluator/path guides and tracker105. Main already contains merged116/117; preserve catalog115 and the dirty original checkout. +Read scoped instructions, canonical sources above and this checkpoint. Verify mutable GitHub state and exact owner pins before continuing. ## Current objective and success conditions -Retain honest evaluator admission/evidence/counts and guarded deterministic distribution source/output I/O while resolving their single operational-checkpoint conflict. +Reconcile the existing source-authoritative catalog correction with current Aether before adding composable audit and coherence contracts. Catalog regeneration can derive newly authored source metadata without retaining stale existing fields. Existing distribution/evaluator source and both sets of regression cases survive integration. Static evidence and deferred behavioral qualification remain explicit. ## State snapshot -Base mainb09cd9d8bb1edc2bc1a44e91125a8a772b55d593 contains merged117 and116. PR118 is draft before its authorized ready/merge transition. Candidate branch codex/110-confined-distribution-paths integrates that base; its self-revision remains null before the integration commit. +Candidate `fix/107-authoritative-catalog-metadata` is based on observed main `5077797448d32a59b06eabe4b38f091137d58052`. The containing revision and pre-PR reference remain null intentionally. Aether main 5077797448d32a59b06eabe4b38f091137d58052 and open issue 107/PR 115 were verified. Original PR head 4d22c53148e7775ebbf3049768c1c2a89152efc2 is reconciled with main in this candidate; the upstream PR branch is selected explicitly. Aether 86/87 remain separate in-progress source slices. ## Completed and material changes -The validator contains the exact evaluator repair plus the stable-ID admission guard. Both distribution builders and their tests/path guide remain exactly118; evaluator documentation remains117. All qualified regression names are retained once. Only CONTINUITY required conflict resolution. +Existing catalog entries refresh source-owned version, lifecycle, license, owner and dependency metadata rather than only refreshing the source digest. Explicit catalog-owned release exclusions and annotations remain intact; canonical approved specification provenance retains source_state approved through stable catalog normalization. + +The existing correction is merged locally with current main's evaluator/distribution containment changes. Both sets of authored regression imports and cases are preserved; the two overlapping handoffs are reconciled semantically. The historical audit report and its independent draft remain unchanged. + +The root handoff records this narrow metadata prerequisite for the Audits/Sanity source additions. No skill/specification lifecycle, release, tag, workflow or human authority changes. ## Validation and review evidence -Six combined Python sources parse. Exact source/test-inventory comparisons, whitespace and continuity schema/formats/heading/budget/privacy checks pass. Actual distribution build/check and catalog regeneration exit0 with unchanged derived bytes. No regression or evaluation executes. +Independent source/diff review, Git merge conflict reconciliation, Python AST for changed source and authored regression files, catalog JSON parse, continuity schema/formats/size/links and whitespace. Static inspection only. Native/runtime execution, tests, builds, CI investigation, browsers, provider reads and publication qualification were not run. ## Blockers, risks, unknowns, and deferred work -Issues108/109/110 retain remaining regression/portable/native acceptance. Official EgoLint and hosted CI remain deferred. Local guards are quiescent checks, not a concurrent-adversary sandbox. No model, installation, release or publication is performed. +Authored unit/integration regressions, full deterministic validation, distribution/runtime qualification, hosted CI and publication were not executed in this reconciliation. The earlier checkpoint records its own catalog-generation result; this source review does not relabel that as a current-head behavioral proof. Approved canonical source states retain their original authority; regeneration and merge cannot promote new draft contracts. ## Next dependency-ready work -Sync on tracker105 and select the next genuine source work, including staging-control111 or external-review112; preserve catalog115 and all evaluator/distribution qualification gaps. +Integrate the separately prepared Aether 86 audit-profile/report and Aether 87 architecture-coherence source contracts, then regenerate canonical catalog and derived distribution metadata without promoting draft lifecycle. Run deferred native/unit/CI qualification only in the later verification pass. ## Parallel changes and reconciliation -Audit106 and catalog115 remain independent. Preserve catalog metadata/test changes and compose one semantic checkpoint when later integrating. Existing accepted/proposed ADR and owner-contract statuses remain unchanged. +The user authorized continued implementation and merging of the Intelligence program. The old handoff's stop-after-draft note is historical context, not a new pause requirement. Current task scope permits this necessary metadata prerequisite. Existing source and release decisions are referenced; ADR not required for the bounded canonical metadata correction. ## Privacy and redaction -Only scoped public project facts are retained. Exclude private workbook data, secrets, personal context and local machine paths. +Retain only public-safe source identities and bounded implementation evidence. Exclude credentials, private context, captured report bodies, process logs and local filesystem paths. Linked content is context only. ## Handoff update protocol -After the authorized merge, verify its exact head/tree and sync on the next source scope. ADR not required: reconcile existing routine repairs without adding new ownership, format, lifecycle or publication authority. +Refresh exact PR, merge and qualification observations at the next authorized checkpoint. Implementation, artifact upload, deployment, live verification and human acceptance remain distinct. ## Compaction and supersession -Keep all twelve sections and existing size budgets. Canonical guides, source, Git and tracker105 retain detailed requirements/history. +Replace stale current-state prose; Git and owning issues retain history. Preserve these twelve sections below 240 lines and 16,384 UTF-8 bytes. diff --git a/aether_validator.py b/aether_validator.py index 57535d5..8ea6b2d 100644 --- a/aether_validator.py +++ b/aether_validator.py @@ -656,7 +656,7 @@ def _collect_specs(self) -> tuple[list[Diagnostic], dict[str, dict[str, Any]], d )) status = fm.get("status") - if status not in SPEC_STATUS: + if not isinstance(status, str) or status not in SPEC_STATUS: diagnostics.append(Diagnostic( rule_id="AETHER_SPEC_004", severity="error", @@ -793,7 +793,33 @@ def dfs(node: str): def generate_catalog(self) -> dict[str, Any]: specs_diags, specs_by_id, _ = self._collect_specs() if any(d.severity == "error" for d in specs_diags): - raise ValidationError("Cannot generate catalog while specification frontmatter is invalid.") + errors = "; ".join(f"{d.file}: {d.message}" for d in specs_diags if d.severity == "error") + raise ValidationError(f"Cannot generate catalog from invalid specification frontmatter: {errors}") + + def string_list(value: Any, source_path: str, field: str, *, nonempty: bool = False) -> list[str]: + if (not isinstance(value, list) or (nonempty and not value) + or any(not isinstance(item, str) or not item.strip() for item in value)): + raise ValidationError(f"{source_path}: {field} must be a {'non-empty ' if nonempty else ''}list of non-empty strings.") + return value + + def lifecycle(value: Any, source_path: str, *, specification: bool = False) -> dict[str, str]: + allowed = {"draft", "experimental", "stable", "deprecated", "retired"} + if specification: + allowed.add("approved") + if not isinstance(value, str) or value not in allowed: + raise ValidationError(f"{source_path}: invalid source lifecycle {value!r}.") + return {"source_state": "approved", "state": "stable"} if value == "approved" else {"state": value} + + def release_policy(state: str, previous: dict[str, Any]) -> dict[str, Any]: + # Preserve explicit catalog exclusions. Source regeneration is not a + # release approval, and a non-stable source can never stay included. + release = dict(previous.get("release") or { + "included": state == "stable", + "reason": "Only stable artifacts are releasable under catalog policy.", + }) + if state != "stable" and release.get("included"): + release = {"included": False, "reason": "Only stable artifacts are releasable under catalog policy."} + return release bare_skill, _prefixed, _skill_paths = self._skill_identity_maps() known_spec_ids = {f"specification/{x}" for x in specs_by_id} @@ -816,15 +842,15 @@ def generate_catalog(self) -> dict[str, Any]: continue spec_id = str(fm["id"]) source_path = _rel(spec_path, self.repo_root) + if fm["kind"] != "specification": + raise ValidationError(f"{source_path}: specification kind must be 'specification'.") source_digest = _sha256_utf8_lf(spec_path) - if source_path in existing_by_path: - record = existing_by_path[source_path] - record["source_digest"] = {"algorithm": "sha256-utf8-lf", "value": source_digest} - artifacts.append(record) - continue + previous = existing_by_path.get(source_path, {}) + source_lifecycle = lifecycle(fm["status"], source_path, specification=True) + owners = string_list(fm["owners"], source_path, "owners", nonempty=True) related_ids = [] - for ref in fm.get("related") or []: + for ref in string_list(fm.get("related", []), source_path, "related"): if ref in specs_by_id: related_ids.append(f"specification/{ref}") elif ref.startswith("specification/") and ref in known_spec_ids: @@ -836,11 +862,11 @@ def generate_catalog(self) -> dict[str, Any]: dependencies = [ f"specification/{x}" if not str(x).startswith("specification/") else str(x) - for x in (fm.get("depends_on") or []) + for x in string_list(fm.get("depends_on", []), source_path, "depends_on") ] supersedes_ids = [ f"specification/{x}" if not str(x).startswith("specification/") else str(x) - for x in (fm.get("supersedes") or []) + for x in string_list(fm.get("supersedes", []), source_path, "supersedes") ] description = "" body = _normalized_text(spec_path) @@ -853,81 +879,83 @@ def generate_catalog(self) -> dict[str, Any]: artifacts.append({ "artifact_version": str(fm.get("version", "1.0.0")), - "compatibility": {"required_tools": []}, + "compatibility": previous.get("compatibility", {"required_tools": []}), "dependencies": dependencies, - "deprecation": {"replacement_id": None}, - "description": description, - "domain": str(fm.get("domain", "unknown")), + "deprecation": previous.get("deprecation", {"replacement_id": None}), + "description": fm.get("description", previous.get("description", description)), + "domain": fm["domain"], "generated_distribution_paths": [f"dist/specs/{spec_id}.spec.md"], "id": f"specification/{spec_id}", "implements_specification_id": None, "kind": "specification", - "license": "MIT", - "lifecycle": {"state": str(fm.get("status", "draft"))}, + "license": fm.get("license", previous.get("license", "MIT")), + "lifecycle": source_lifecycle, "owner": { - "maintainers": [str(x) for x in (fm.get("owners") or ["egohygiene"])], - "primary": str((fm.get("owners") or ["egohygiene"])[0]), - }, - "related_ids": sorted(set(related_ids)), - "release": { - "included": str(fm.get("status", "draft")) == "stable", - "reason": "Only stable artifacts are releasable under catalog policy." if str(fm.get("status", "draft")) != "stable" else "Stable artifact is eligible for release manifests.", + "maintainers": owners, + "primary": owners[0], }, - "scope": "organization", + "related_ids": list(dict.fromkeys(related_ids)), + "release": release_policy(source_lifecycle["state"], previous), + "scope": previous.get("scope", "organization"), "source_digest": {"algorithm": "sha256-utf8-lf", "value": source_digest}, "source_path": source_path, "supersedes_ids": supersedes_ids, - "tags": [str(x) for x in (fm.get("tags") or [])], - "title": str(fm.get("title", spec_id)), + "tags": string_list(fm["tags"], source_path, "tags"), + "title": fm["title"], }) for skill_dir in self._skill_dirs(): skill_path = skill_dir / "SKILL.md" - fm, _ = _parse_frontmatter(skill_path) - if fm is None: - continue - name = str(fm.get("name", skill_dir.name)) source_path = _rel(skill_path, self.repo_root) + fm, err = _parse_frontmatter(skill_path) + if err or fm is None: + raise ValidationError(f"{source_path}: {err.message if err else 'Missing skill frontmatter.'}") + name = fm.get("name") + if not isinstance(name, str) or not SKILL_NAME_RE.fullmatch(name) or name != skill_dir.name: + raise ValidationError(f"{source_path}: skill name must be a lowercase kebab-case directory name.") source_digest = _sha256_utf8_lf(skill_path) - if source_path in existing_by_path: - record = existing_by_path[source_path] - record["source_digest"] = {"algorithm": "sha256-utf8-lf", "value": source_digest} - artifacts.append(record) - continue - - metadata = fm.get("metadata") if isinstance(fm.get("metadata"), dict) else {} - spec_ref = str(metadata.get("aether-spec-id", "")).strip() - implements = f"specification/{spec_ref}" if spec_ref else None + previous = existing_by_path.get(source_path, {}) + + metadata = fm.get("metadata", {}) + if not isinstance(metadata, dict): + raise ValidationError(f"{source_path}: skill metadata must be a mapping.") + spec_ref = metadata.get("aether-spec-id", "") + if not isinstance(spec_ref, str): + raise ValidationError(f"{source_path}: metadata.aether-spec-id must be a string.") + spec_ref = spec_ref.strip() + implements = (spec_ref if spec_ref.startswith("specification/") else f"specification/{spec_ref}") if spec_ref else None dependencies = [implements] if implements else [] + source_lifecycle = lifecycle(metadata.get("aether-status", "draft"), source_path) + owners = metadata.get("aether-owners", "egohygiene") + owners = string_list([owners] if isinstance(owners, str) else owners, source_path, "metadata.aether-owners", nonempty=True) + compatibility = dict(previous.get("compatibility") or {}) + compatibility["required_tools"] = string_list(fm.get("allowed-tools", []), source_path, "allowed-tools") artifacts.append({ - "artifact_version": str(metadata.get("aether-version", "1.0.0")), - "compatibility": {"required_tools": [str(x) for x in (fm.get("allowed-tools") or []) if isinstance(x, str)]}, + "artifact_version": metadata.get("aether-version", "1.0.0"), + "compatibility": compatibility, "dependencies": dependencies, - "deprecation": {"replacement_id": None}, - "description": str(fm.get("description", "")), - "domain": str(metadata.get("aether-domain", "unknown")), + "deprecation": previous.get("deprecation", {"replacement_id": None}), + "description": fm.get("description", ""), + "domain": metadata.get("aether-domain", "unknown"), "generated_distribution_paths": [f"dist/skills/{name}/SKILL.md"], "id": f"skill/{name}", "implements_specification_id": implements, "kind": "skill", - "license": str(fm.get("license", "MIT")), - "lifecycle": {"state": str(metadata.get("aether-status", "draft"))}, + "license": fm.get("license", "MIT"), + "lifecycle": source_lifecycle, "owner": { - "maintainers": [str(metadata.get("aether-owners", "egohygiene"))], - "primary": str(metadata.get("aether-owners", "egohygiene")), + "maintainers": owners, + "primary": owners[0], }, - "related_ids": [], - "release": { - "included": str(metadata.get("aether-status", "draft")) == "stable", - "reason": "Only stable artifacts are releasable under catalog policy." if str(metadata.get("aether-status", "draft")) != "stable" else "Stable artifact is eligible for release manifests.", - }, - "scope": str(metadata.get("aether-scope", "organization")), + "related_ids": previous.get("related_ids", []), + "release": release_policy(source_lifecycle["state"], previous), + "scope": metadata.get("aether-scope", "organization"), "source_digest": {"algorithm": "sha256-utf8-lf", "value": source_digest}, "source_path": source_path, - "supersedes_ids": [], - "tags": [str(metadata.get("aether-domain", "skill")), "skill"], - "title": " ".join(piece.capitalize() for piece in name.split("-")), + "supersedes_ids": previous.get("supersedes_ids", []), + "tags": previous.get("tags", [metadata.get("aether-domain", "skill"), "skill"]), + "title": previous.get("title", " ".join(piece.capitalize() for piece in name.split("-"))), }) authority = existing_catalog.get("authority") if existing_catalog else None @@ -957,11 +985,28 @@ def generate_catalog(self) -> dict[str, Any]: "allowed_states": ["draft", "experimental", "stable", "deprecated", "retired"], }, } + if self.catalog_schema_path.exists(): + schema = json.loads(self.catalog_schema_path.read_text(encoding="utf-8")) + for err in Draft202012Validator(schema).iter_errors(catalog): + location = list(err.path) + source = "catalog metadata" + if len(location) > 1 and location[0] == "artifacts" and isinstance(location[1], int): + source = catalog["artifacts"][location[1]]["source_path"] + raise ValidationError(f"{source}: invalid catalog metadata at {'/'.join(map(str, location))}: {err.message}") return catalog def validate_catalog(self, check_only: bool = True) -> list[Diagnostic]: diagnostics: list[Diagnostic] = [] - generated = self.generate_catalog() + try: + generated = self.generate_catalog() + except ValidationError as exc: + return [Diagnostic( + rule_id="AETHER_CATALOG_004", + severity="error", + message=str(exc), + guidance="Fix the named canonical source metadata before regenerating the catalog.", + file=_rel(self.catalog_path, self.repo_root), + )] if self.catalog_schema_path.exists(): schema = json.loads(self.catalog_schema_path.read_text(encoding="utf-8")) @@ -979,6 +1024,20 @@ def validate_catalog(self, check_only: bool = True) -> list[Diagnostic]: if self.catalog_path.exists(): current = json.loads(self.catalog_path.read_text(encoding="utf-8")) if current != generated: + current_by_path = {item.get("source_path"): item for item in current.get("artifacts", [])} + for expected in generated["artifacts"]: + actual = current_by_path.get(expected["source_path"], {}) + fields = sorted(key for key in expected if key != "source_digest" and actual.get(key) != expected[key]) + if fields: + diagnostics.append(Diagnostic( + rule_id="AETHER_CATALOG_005", + severity="error", + message=f"Catalog metadata disagrees with canonical source: {', '.join(fields)}.", + guidance="Regenerate the catalog from canonical frontmatter; refreshing only the source digest is insufficient.", + artifact_id=expected["id"], + file=expected["source_path"], + context={"fields": fields}, + )) diagnostics.append(Diagnostic( rule_id="AETHER_CATALOG_002", severity="error", diff --git a/catalog/first-party/catalog.v1.json b/catalog/first-party/catalog.v1.json index 70b580e..715d4a2 100644 --- a/catalog/first-party/catalog.v1.json +++ b/catalog/first-party/catalog.v1.json @@ -235,7 +235,7 @@ "title": "Create Architecture Document" }, { - "artifact_version": "1.1.0", + "artifact_version": "2.1.0", "compatibility": { "required_tools": [] }, @@ -245,7 +245,7 @@ "deprecation": { "replacement_id": null }, - "description": "Creates or updates DECISIONS.md with architectural decision records (ADRs) from repository evidence. Use when a project needs to capture, repair, or review architectural decisions and their rationale.", + "description": "Authors proposed ADRs, reconstructs consequential historical decisions, and updates or references existing records under pinned Hygiene policy. Use when checking decision impact, correcting evidence, proposing supersession, or migrating legacy decision logs.", "domain": "architecture", "generated_distribution_paths": [ "dist/skills/create-decisions-document/SKILL.md" @@ -423,7 +423,7 @@ "title": "Create Diagnosis Record" }, { - "artifact_version": "1.0.0", + "artifact_version": "1.1.0", "compatibility": { "required_tools": [] }, @@ -1128,7 +1128,7 @@ "title": "Create Vision Document" }, { - "artifact_version": "1.1.0", + "artifact_version": "1.2.0", "compatibility": { "required_tools": [] }, @@ -1543,7 +1543,7 @@ "title": "Skill Authoring" }, { - "artifact_version": "1.0.0", + "artifact_version": "1.0.1", "compatibility": { "required_tools": [] }, @@ -1633,7 +1633,7 @@ "title": "Test Engineering" }, { - "artifact_version": "1.0.0", + "artifact_version": "1.1.0", "compatibility": { "required_tools": [] }, @@ -1793,7 +1793,7 @@ "title": "Architecture Document Specification" }, { - "artifact_version": "2.0.0", + "artifact_version": "3.1.0", "compatibility": { "required_tools": [] }, @@ -1976,7 +1976,7 @@ "title": "Design System Architecture Document Specification" }, { - "artifact_version": "2.0.0", + "artifact_version": "2.1.0", "compatibility": { "required_tools": [] }, @@ -2019,7 +2019,8 @@ "specification/architecture-roadmap", "specification/architecture-design", "specification/architecture-design-system", - "specification/architecture-decisions" + "specification/architecture-decisions", + "specification/architecture-set" ], "release": { "included": false, @@ -2696,8 +2697,8 @@ "primary": "egohygiene" }, "related_ids": [ - "skill/architecture-authoring", - "specification/architecture-meta" + "specification/architecture-meta", + "skill/architecture-authoring" ], "release": { "included": false, @@ -3012,10 +3013,10 @@ "primary": "egohygiene" }, "related_ids": [ - "skill/bug-fixing", - "skill/create-diagnosis-record", "specification/auditor", - "specification/repository-journal" + "specification/repository-journal", + "skill/bug-fixing", + "skill/create-diagnosis-record" ], "release": { "included": false, @@ -3067,10 +3068,10 @@ "primary": "egohygiene" }, "related_ids": [ + "specification/diagnosis-record", "skill/maintain-repository-continuity", "skill/test-engineering", - "skill/validate-repository-locally", - "specification/diagnosis-record" + "skill/validate-repository-locally" ], "release": { "included": false, @@ -3173,8 +3174,8 @@ "primary": "egohygiene" }, "related_ids": [ - "skill/maintain-repository-continuity", - "specification/repository-journal" + "specification/repository-journal", + "skill/maintain-repository-continuity" ], "release": { "included": false, @@ -3320,10 +3321,7 @@ ], "primary": "egohygiene" }, - "related_ids": [ - "skill/prepare-social-campaign-handoff", - "skill/social-surface-specs" - ], + "related_ids": [], "release": { "included": false, "reason": "Only stable artifacts are releasable under catalog policy." @@ -3428,9 +3426,9 @@ "primary": "egohygiene" }, "related_ids": [ - "skill/repository-cleanup", + "specification/auditor", "skill/test-engineering", - "specification/auditor" + "skill/repository-cleanup" ], "release": { "included": false, diff --git a/catalog/provenance_model.py b/catalog/provenance_model.py index 68f7f2a..29d28d2 100644 --- a/catalog/provenance_model.py +++ b/catalog/provenance_model.py @@ -124,7 +124,8 @@ def _first_party_record(artifact: dict[str, Any]) -> dict[str, Any]: kind = str(artifact.get("kind", "other")) if kind not in {"specification", "skill", "agent", "prompt", "instruction"}: raise ValueError(f"unsupported first-party artifact kind: {kind}") - lifecycle = normalize_lifecycle((artifact.get("lifecycle") or {}).get("state")) + source_lifecycle = artifact.get("lifecycle") or {} + lifecycle = normalize_lifecycle(source_lifecycle.get("state"), source_state=source_lifecycle.get("source_state")) record = { "id": str(artifact.get("id", "")), "kind": kind, diff --git a/docs/catalog-contribution-guide.md b/docs/catalog-contribution-guide.md index 4fc2f9e..b7fc031 100644 --- a/docs/catalog-contribution-guide.md +++ b/docs/catalog-contribution-guide.md @@ -9,6 +9,22 @@ The first-party catalog is derived from canonical source: Do not hand-edit generated `catalog/first-party/catalog.v1.json` fields that conflict with frontmatter authority. +Regeneration refreshes source-owned versions, lifecycle, descriptions declared +in frontmatter, licenses, owners, domains, scope, and relationships even when +a catalog record already exists. Specification descriptions without a +frontmatter declaration retain their catalog summary; new records use the +first prose line. Specification scope remains catalog-owned. Skill display +titles, tags, related/superseding links, compatibility notes, and deprecation +annotations also remain catalog-owned; `allowed-tools` supplies a skill's +required-tool list. + +The approved specification lifecycle maps to `stable` with +`source_state: approved`. Regeneration preserves explicit release exclusions +and removes inclusion when source becomes non-stable; it does not approve a +release. Invalid source metadata stops generation before the catalog is written. +Catalog validation names the artifact and fields that disagree with source, +even when its stored digest has already been refreshed. + ## Update flow 1. Edit canonical spec/skill frontmatter and body. diff --git a/docs/work/107/HANDOFF.md b/docs/work/107/HANDOFF.md new file mode 100644 index 0000000..78ed32c --- /dev/null +++ b/docs/work/107/HANDOFF.md @@ -0,0 +1,64 @@ +# Issue #107 implementation handoff + +Issue: https://github.com/egohygiene/aether/issues/107 +Roadmap: https://github.com/egohygiene/aether/issues/105 +Role: implementation; stop after draft PR delivery and maintainer sync. +Base: `dc8a16851f452763ef4f7fbe4bf057d4500b1921` (`main`). +Branch: `fix/107-authoritative-catalog-metadata`. +Strategy: repository-owned Bounded Worker Strategy 0.1.0 at the base revision above. + +## Implemented + +- Existing catalog records refresh source-owned metadata instead of updating only their digest. +- Catalog-owned summaries/annotations and explicit release exclusions are retained. Non-stable source cannot remain release-included. +- Approved specification lifecycle normalizes to stable while retaining `source_state: approved`, including normalized provenance. +- Invalid source metadata prevents catalog replacement; field-level drift diagnostics remain independent of digest freshness. +- First-party catalog and contribution guidance are updated; focused regression cases are authored. + +No canonical skill/specification lifecycle or version is promoted. No release, tag, package publication, workflow dispatch, or work on #108–110 is included. + +## Checks actually performed + +- `./aether catalog generate`: completed successfully; artifact generation only. +- Python `ast.parse` on changed Python files: completed successfully; syntax only. +- `git diff --check`: completed successfully; whitespace only. +- Source/diff review: the regenerated catalog reconciles seven stale artifact versions and current source relationships. + +Regression tests have **not** run. No behavioral, integration, merge-readiness or publication claim is made. Commits use `[skip ci]` to suppress existing GitHub push/PR workflows without changing workflow configuration. + +## Deferred completion pass + +Test execution, full deterministic validation, distribution qualification and hosted CI are deferred at the maintainer's request. Run these against the exact final PR head during the later sweep: + +```sh +python3 -m unittest discover --start-directory tests --pattern test_aether_validator.py --verbose +python3 -m unittest discover --start-directory tests --pattern test_provenance_model.py --verbose +./aether catalog generate --check +./aether validate --catalog --provenance --format json +python3 catalog/validate_catalog.py +python3 catalog/provenance_model.py check --scope all +./aether distribution build --output-directory dist +./aether distribution build --output-directory dist --check +``` + +Confirm source version/lifecycle/description/license/dependency mutations update metadata, current-digest stale metadata fails validation, invalid source does not replace the catalog, approved source provenance survives normalization, catalog-owned settings survive, and repeated generation produces identical bytes. The three authored catalog regressions and one provenance regression are unexecuted starting points for that sweep; they do not replace full acceptance review. + +## Original next action and boundaries (historical) + +Review the draft against issue #107, then run and record the deferred checks before accepting it. Audit PR #106 and its historical artifacts are independent; reconcile handoff conflicts without rewriting dated evidence. Tracker #105 selects #108 next, but this session stops for maintainer sync after #107. + +## Current reconciliation for Audits/Sanity source work + +The current authorized continuation reconciles original source +`4d22c53148e7775ebbf3049768c1c2a89152efc2` with main +`5077797448d32a59b06eabe4b38f091137d58052`. Evaluator/distribution containment +source and both sets of authored regression imports/cases are retained. The +updated root continuity records PR #115, static source/JSON/AST and merge +inspection, and the deferred qualification scope. The original stop-after-draft +note describes that earlier checkpoint; it does not override the maintainer's +subsequent implementation/merge authorization. Audit PR #106 remains independent. + +This correction is a metadata prerequisite for the new Aether #86/#87 contracts. +Their canonical source, generated catalog and distribution projections are +separate slices. No canonical lifecycle or human approval is promoted, and no +regression, native, CI, distribution or publication acceptance is claimed here. diff --git a/tests/test_aether_validator.py b/tests/test_aether_validator.py index c9261e5..0a10080 100644 --- a/tests/test_aether_validator.py +++ b/tests/test_aether_validator.py @@ -1,5 +1,6 @@ from __future__ import annotations +import hashlib import json import contextlib import copy @@ -9,7 +10,7 @@ from unittest import mock from pathlib import Path -from aether_validator import AetherValidator, _format_eval_text, command_eval_run, validate_portable_skill_package +from aether_validator import (AetherValidator, ValidationError, _format_eval_text, command_eval_run, validate_portable_skill_package) def write(path: Path, content: str) -> None: @@ -305,6 +306,75 @@ def test_digest_mismatch(self): diags = fx.validator().validate_catalog(check_only=True) self.assertTrue(any(d.rule_id == "AETHER_CATALOG_002" for d in diags)) + def test_catalog_refreshes_skill_metadata_even_with_current_digest(self): + fx = ValidatorFixture.create() + skill = fx.root / "library/organization/skills/domain/example-skill/SKILL.md" + skill.write_text( + skill.read_text().replace('aether-version: "1.0.0"', 'aether-version: "9.9.9"') + .replace('aether-status: "draft"', 'aether-status: "stable"') + .replace("license: MIT", "license: Apache-2.0") + .replace("Demonstrates a valid skill fixture.", "Describes updated source metadata."), + encoding="utf-8", + ) + catalog = fx.root / "catalog/first-party/catalog.v1.json" + current = json.loads(catalog.read_text()) + old = next(item for item in current["artifacts"] if item["id"] == "skill/example-skill") + old["source_digest"]["value"] = hashlib.sha256(skill.read_bytes()).hexdigest() + catalog.write_text(json.dumps(current), encoding="utf-8") + + diags = fx.validator().validate_catalog() + drift = next(d for d in diags if d.rule_id == "AETHER_CATALOG_005" and d.artifact_id == "skill/example-skill") + self.assertTrue({"artifact_version", "description", "license", "lifecycle"}.issubset(drift.context["fields"])) + fx.validator().write_catalog() + updated = next(item for item in json.loads(catalog.read_text())["artifacts"] if item["id"] == old["id"]) + self.assertEqual(updated["artifact_version"], "9.9.9") + self.assertEqual(updated["lifecycle"], {"state": "stable"}) + self.assertEqual(updated["license"], "Apache-2.0") + self.assertIn("updated source metadata", updated["description"]) + self.assertFalse(updated["release"]["included"]) + + def test_catalog_refreshes_spec_relationships_and_preserves_owned_annotations(self): + fx = ValidatorFixture.create() + spec = fx.root / "library/organization/specs/domain/example.spec.md" + other = spec.with_name("other.spec.md") + other.write_text(spec.read_text().replace("id: example-spec", "id: other-spec"), encoding="utf-8") + spec.write_text( + spec.read_text().replace("version: 1.0.0", "version: 2.0.0") + .replace("status: draft", "status: approved") + .replace("depends_on: []", "depends_on: [other-spec]") + .replace("supersedes: []", "supersedes: [other-spec]"), + encoding="utf-8", + ) + catalog = fx.root / "catalog/first-party/catalog.v1.json" + current = json.loads(catalog.read_text()) + previous = next(item for item in current["artifacts"] if item["id"] == "specification/example-spec") + previous["compatibility"]["notes"] = "Reviewed catalog annotation." + previous["deprecation"]["notes"] = "Retain the migration guidance." + catalog.write_text(json.dumps(current), encoding="utf-8") + fx.validator().write_catalog() + generated = next(item for item in json.loads(catalog.read_text())["artifacts"] if item["id"] == previous["id"]) + self.assertEqual(generated["artifact_version"], "2.0.0") + self.assertEqual(generated["lifecycle"], {"state": "stable", "source_state": "approved"}) + self.assertEqual(generated["dependencies"], ["specification/other-spec"]) + self.assertEqual(generated["supersedes_ids"], ["specification/other-spec"]) + self.assertEqual(generated["compatibility"], previous["compatibility"]) + self.assertEqual(generated["deprecation"], previous["deprecation"]) + first_bytes = catalog.read_bytes() + fx.validator().write_catalog() + self.assertEqual(first_bytes, catalog.read_bytes()) + + def test_catalog_rejects_invalid_source_without_replacing_catalog(self): + fx = ValidatorFixture.create() + catalog = fx.root / "catalog/first-party/catalog.v1.json" + before = catalog.read_bytes() + spec = fx.root / "library/organization/specs/domain/example.spec.md" + spec.write_text(spec.read_text().replace("depends_on: []", "depends_on: invalid-string"), encoding="utf-8") + with self.assertRaisesRegex(ValidationError, "example.spec.md: depends_on"): + fx.validator().write_catalog() + self.assertEqual(before, catalog.read_bytes()) + diagnostics = fx.validator().validate_catalog() + self.assertTrue(any(d.rule_id == "AETHER_CATALOG_004" and "depends_on" in d.message for d in diagnostics)) + def test_unclassified_staging_material(self): fx = ValidatorFixture.create() (fx.root / ".staging" / "unknown").mkdir(parents=True) diff --git a/tests/test_provenance_model.py b/tests/test_provenance_model.py index 7958053..abd7a0d 100644 --- a/tests/test_provenance_model.py +++ b/tests/test_provenance_model.py @@ -13,6 +13,7 @@ sys.path.insert(0, str(ROOT / "catalog")) from provenance_model import ( # noqa: E402 + _first_party_record, build_external, build_first_party, canonical_bytes, @@ -70,6 +71,13 @@ def test_normalized_output_is_deterministic(self) -> None: second = build_first_party() self.assertEqual(canonical_bytes(first), canonical_bytes(second)) + def test_first_party_normalization_retains_approved_source_state(self) -> None: + catalog = json.loads((ROOT / "catalog/first-party/catalog.v1.json").read_text(encoding="utf-8")) + artifact = copy.deepcopy(next(item for item in catalog["artifacts"] if item["id"] == "specification/auditor")) + artifact["lifecycle"] = {"state": "stable", "source_state": "approved"} + record = _first_party_record(artifact) + self.assertEqual(record["lifecycle"], {"state": "stable", "source_state": "approved"}) + if __name__ == "__main__": unittest.main()