diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4b7f85d --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +PYTHON ?= python3 +CANON_CLI = PYTHONPATH=src $(PYTHON) -m info_tech_canon + +.PHONY: validate index tree agent-briefs test + +validate: + $(CANON_CLI) validate --write infospace/validation/latest.json + +index: + $(CANON_CLI) index + +tree: + $(CANON_CLI) tree + +agent-briefs: + $(CANON_CLI) agent-briefs + +test: + $(PYTHON) -m pytest diff --git a/README.md b/README.md index 55931a3..2cf5df0 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,8 @@ PYTHONPATH=src python3 -m info_tech_canon models PYTHONPATH=src python3 -m info_tech_canon standards PYTHONPATH=src python3 -m info_tech_canon validate PYTHONPATH=src python3 -m info_tech_canon graph +PYTHONPATH=src python3 -m info_tech_canon index +PYTHONPATH=src python3 -m info_tech_canon views PYTHONPATH=src python3 -m info_tech_canon api --host 127.0.0.1 --port 8765 ``` @@ -38,4 +40,15 @@ After package installation, the same commands are available through the - `GET /validate` - `GET /graph` - `GET /graph?format=mermaid` +- `GET /views` +- `GET /views/{name}` - `GET /profiles/{profile}/inspect` + +## Maintenance + +```bash +make validate +make index +make tree +make agent-briefs +``` diff --git a/canon.yaml b/canon.yaml index e8a33cf..5f9032d 100644 --- a/canon.yaml +++ b/canon.yaml @@ -108,6 +108,7 @@ planned_directories: - infospace/assimilation/ - infospace/schemas/ - infospace/views/ + - infospace/indexes/ - infospace/agent/ - infospace/examples/ - infospace/validation/ diff --git a/infospace/agent/global-agent-brief.md b/infospace/agent/global-agent-brief.md new file mode 100644 index 0000000..d7e349f --- /dev/null +++ b/infospace/agent/global-agent-brief.md @@ -0,0 +1,23 @@ + + +# Global Agent Brief + +This brief summarizes the current canon service surface for agents. + +- Infospace slug: `canon` +- Artifact count: 15 +- Primary confidence command: `make validate` +- Refresh generated indexes and views with: `make index` + +## Useful Commands + +- `PYTHONPATH=src python3 -m info_tech_canon inspect` +- `PYTHONPATH=src python3 -m info_tech_canon validate` +- `PYTHONPATH=src python3 -m info_tech_canon graph` +- `PYTHONPATH=src python3 -m info_tech_canon index` + +## Consumption Notes + +- Treat `seeds/` as provenance. +- Treat `infospace/` as the service-consumable canon root. +- Generated files are marked and can be refreshed deterministically. diff --git a/infospace/indexes/README.md b/infospace/indexes/README.md new file mode 100644 index 0000000..a956ea1 --- /dev/null +++ b/infospace/indexes/README.md @@ -0,0 +1,3 @@ +# Indexes + +Generated machine-readable indexes live here. diff --git a/infospace/indexes/artifact-tree.yaml b/infospace/indexes/artifact-tree.yaml new file mode 100644 index 0000000..7b73015 --- /dev/null +++ b/infospace/indexes/artifact-tree.yaml @@ -0,0 +1,153 @@ +root: infospace +file_count: 50 +files: +- path: README.md + directory: . + name: README.md +- path: agent/README.md + directory: agent + name: README.md +- path: agent/global-agent-brief.md + directory: agent + name: global-agent-brief.md +- path: artifacts/index.yaml + directory: artifacts + name: index.yaml +- path: assimilation/README.md + directory: assimilation + name: README.md +- path: examples/README.md + directory: examples + name: README.md +- path: indexes/README.md + directory: indexes + name: README.md +- path: indexes/artifact-tree.yaml + directory: indexes + name: artifact-tree.yaml +- path: indexes/concept-ownership.yaml + directory: indexes + name: concept-ownership.yaml +- path: indexes/import-matrix.yaml + directory: indexes + name: import-matrix.yaml +- path: infospace.yaml + directory: . + name: infospace.yaml +- path: kernel/InfoTechCanonCore.md + directory: kernel + name: InfoTechCanonCore.md +- path: kernel/InfoTechCanonKernelMap.md + directory: kernel + name: InfoTechCanonKernelMap.md +- path: mappings/README.md + directory: mappings + name: README.md +- path: models/access-control/InfoTechCanonAccessControlModel.md + directory: models/access-control + name: InfoTechCanonAccessControlModel.md +- path: models/data/InfoTechCanonDataModel.md + directory: models/data + name: InfoTechCanonDataModel.md +- path: models/devsecops/InfoTechCanonDevSecOpsModel.md + directory: models/devsecops + name: InfoTechCanonDevSecOpsModel.md +- path: models/governance/InfoTechCanonGovernanceModel.md + directory: models/governance + name: InfoTechCanonGovernanceModel.md +- path: models/information-space/InfoTechCanonInformationSpaceModel.md + directory: models/information-space + name: InfoTechCanonInformationSpaceModel.md +- path: models/landscape/InfoTechCanonLandscapeModel.md + directory: models/landscape + name: InfoTechCanonLandscapeModel.md +- path: models/network/InfoTechCanonNetworkModel.md + directory: models/network + name: InfoTechCanonNetworkModel.md +- path: models/observability/InfoTechCanonObservabilityModel.md + directory: models/observability + name: InfoTechCanonObservabilityModel.md +- path: models/organization/InfoTechCanonOrganizationModel.md + directory: models/organization + name: InfoTechCanonOrganizationModel.md +- path: models/security/InfoTechCanonSecurityModel.md + directory: models/security + name: InfoTechCanonSecurityModel.md +- path: models/task/InfoTechCanonTaskModel.md + directory: models/task + name: InfoTechCanonTaskModel.md +- path: patterns/README.md + directory: patterns + name: README.md +- path: profiles/README.md + directory: profiles + name: README.md +- path: reports/scaffold-placement.md + directory: reports + name: scaffold-placement.md +- path: schemas/README.md + directory: schemas + name: README.md +- path: schemas/agent-brief.schema.yaml + directory: schemas + name: agent-brief.schema.yaml +- path: schemas/assimilation.schema.yaml + directory: schemas + name: assimilation.schema.yaml +- path: schemas/concept.schema.yaml + directory: schemas + name: concept.schema.yaml +- path: schemas/index.yaml + directory: schemas + name: index.yaml +- path: schemas/interface-card.schema.yaml + directory: schemas + name: interface-card.schema.yaml +- path: schemas/mapping.schema.yaml + directory: schemas + name: mapping.schema.yaml +- path: schemas/profile.schema.yaml + directory: schemas + name: profile.schema.yaml +- path: schemas/standard.schema.yaml + directory: schemas + name: standard.schema.yaml +- path: schemas/workplan.schema.yaml + directory: schemas + name: workplan.schema.yaml +- path: standards/caring/InfoTechCanonCaringAccessGovernanceStandard.md + directory: standards/caring + name: InfoTechCanonCaringAccessGovernanceStandard.md +- path: standards/tagging/InfoTechCanonTaggingStandard.md + directory: standards/tagging + name: InfoTechCanonTaggingStandard.md +- path: validation/README.md + directory: validation + name: README.md +- path: validation/latest.json + directory: validation + name: latest.json +- path: views/README.md + directory: views + name: README.md +- path: views/by-concept.md + directory: views + name: by-concept.md +- path: views/by-mapping-target.md + directory: views + name: by-mapping-target.md +- path: views/by-profile.md + directory: views + name: by-profile.md +- path: views/by-standard.md + directory: views + name: by-standard.md +- path: views/import-matrix.md + directory: views + name: import-matrix.md +- path: views/kernel-overview.md + directory: views + name: kernel-overview.md +- path: views/repository-tree.md + directory: views + name: repository-tree.md diff --git a/infospace/indexes/concept-ownership.yaml b/infospace/indexes/concept-ownership.yaml new file mode 100644 index 0000000..bea5624 --- /dev/null +++ b/infospace/indexes/concept-ownership.yaml @@ -0,0 +1,124 @@ +concept_count: 30 +concepts: +- concept: InfoTechCanon Core + owner: kernel/itc-core + path: kernel/InfoTechCanonCore.md + source: artifact_title +- concept: InfoTechCanon Kernel Map + owner: kernel/itc-kernel-map + path: kernel/InfoTechCanonKernelMap.md + source: artifact_title +- concept: InfoTechCanon Access Control Model + owner: model/access-control + path: models/access-control/InfoTechCanonAccessControlModel.md + source: artifact_title +- concept: InfoTechCanon Data Model + owner: model/data + path: models/data/InfoTechCanonDataModel.md + source: artifact_title +- concept: InfoTechCanon DevSecOps Model + owner: model/devsecops + path: models/devsecops/InfoTechCanonDevSecOpsModel.md + source: artifact_title +- concept: InfoTechCanon Governance Model + owner: model/governance + path: models/governance/InfoTechCanonGovernanceModel.md + source: artifact_title +- concept: InfoTechCanon Information Space Model + owner: model/information-space + path: models/information-space/InfoTechCanonInformationSpaceModel.md + source: artifact_title +- concept: InfoTechCanon Landscape Model + owner: model/landscape + path: models/landscape/InfoTechCanonLandscapeModel.md + source: artifact_title +- concept: InfoTechCanon Network Model + owner: model/network + path: models/network/InfoTechCanonNetworkModel.md + source: artifact_title +- concept: InfoTechCanon Observability Model + owner: model/observability + path: models/observability/InfoTechCanonObservabilityModel.md + source: artifact_title +- concept: InfoTechCanon Organization Model + owner: model/organization + path: models/organization/InfoTechCanonOrganizationModel.md + source: artifact_title +- concept: InfoTechCanon Security Model + owner: model/security + path: models/security/InfoTechCanonSecurityModel.md + source: artifact_title +- concept: InfoTechCanon Task Model + owner: model/task + path: models/task/InfoTechCanonTaskModel.md + source: artifact_title +- concept: InfoTechCanon CARING Access Governance Standard + owner: standard/caring + path: standards/caring/InfoTechCanonCaringAccessGovernanceStandard.md + source: artifact_title +- concept: CARINGAccessDescriptor + owner: standard/caring + path: standards/caring/InfoTechCanonCaringAccessGovernanceStandard.md + source: frontmatter.owned_concepts +- concept: CARINGCanonicalRole + owner: standard/caring + path: standards/caring/InfoTechCanonCaringAccessGovernanceStandard.md + source: frontmatter.owned_concepts +- concept: CARINGOrganizationRelation + owner: standard/caring + path: standards/caring/InfoTechCanonCaringAccessGovernanceStandard.md + source: frontmatter.owned_concepts +- concept: CARINGPlane + owner: standard/caring + path: standards/caring/InfoTechCanonCaringAccessGovernanceStandard.md + source: frontmatter.owned_concepts +- concept: CARINGCapabilityProfile + owner: standard/caring + path: standards/caring/InfoTechCanonCaringAccessGovernanceStandard.md + source: frontmatter.owned_concepts +- concept: CARINGExposureMode + owner: standard/caring + path: standards/caring/InfoTechCanonCaringAccessGovernanceStandard.md + source: frontmatter.owned_concepts +- concept: CARINGExposureEvent + owner: standard/caring + path: standards/caring/InfoTechCanonCaringAccessGovernanceStandard.md + source: frontmatter.owned_concepts +- concept: CARINGDeclaredAccessMap + owner: standard/caring + path: standards/caring/InfoTechCanonCaringAccessGovernanceStandard.md + source: frontmatter.owned_concepts +- concept: CARINGEffectiveAccessMap + owner: standard/caring + path: standards/caring/InfoTechCanonCaringAccessGovernanceStandard.md + source: frontmatter.owned_concepts +- concept: CARINGDerivedCapability + owner: standard/caring + path: standards/caring/InfoTechCanonCaringAccessGovernanceStandard.md + source: frontmatter.owned_concepts +- concept: CARINGInducedAccess + owner: standard/caring + path: standards/caring/InfoTechCanonCaringAccessGovernanceStandard.md + source: frontmatter.owned_concepts +- concept: CARINGRestrictionPrecedence + owner: standard/caring + path: standards/caring/InfoTechCanonCaringAccessGovernanceStandard.md + source: frontmatter.owned_concepts +- concept: CARINGAnalysisFitnessTest + owner: standard/caring + path: standards/caring/InfoTechCanonCaringAccessGovernanceStandard.md + source: frontmatter.owned_concepts +- concept: CARINGAnalysisProcedure + owner: standard/caring + path: standards/caring/InfoTechCanonCaringAccessGovernanceStandard.md + source: frontmatter.owned_concepts +- concept: CARINGRedesignProcedure + owner: standard/caring + path: standards/caring/InfoTechCanonCaringAccessGovernanceStandard.md + source: frontmatter.owned_concepts +- concept: InfoTechCanon Tagging Standard + owner: standard/tagging + path: standards/tagging/InfoTechCanonTaggingStandard.md + source: artifact_title +duplicate_candidates: [] +ownership_conflicts: [] diff --git a/infospace/indexes/import-matrix.yaml b/infospace/indexes/import-matrix.yaml new file mode 100644 index 0000000..5f505b1 --- /dev/null +++ b/infospace/indexes/import-matrix.yaml @@ -0,0 +1,137 @@ +artifacts: +- kernel/itc-core +- kernel/itc-kernel-map +- model/access-control +- model/data +- model/devsecops +- model/governance +- model/information-space +- model/landscape +- model/network +- model/observability +- model/organization +- model/security +- model/task +- standard/caring +- standard/tagging +rows: +- artifact: kernel/itc-core + targets: {} +- artifact: kernel/itc-kernel-map + targets: + kernel/itc-core: + - maps + model/access-control: + - maps + model/data: + - maps + model/devsecops: + - maps + model/governance: + - maps + model/information-space: + - maps + model/landscape: + - maps + model/network: + - maps + model/observability: + - maps + model/organization: + - maps + model/security: + - maps + model/task: + - maps + standard/caring: + - maps + standard/tagging: + - maps +- artifact: model/access-control + targets: + kernel/itc-core: + - conforms_to + model/governance: + - uses + model/organization: + - uses +- artifact: model/data + targets: + kernel/itc-core: + - conforms_to + model/governance: + - uses +- artifact: model/devsecops + targets: + kernel/itc-core: + - conforms_to + model/security: + - uses +- artifact: model/governance + targets: + kernel/itc-core: + - conforms_to +- artifact: model/information-space + targets: + kernel/itc-core: + - conforms_to +- artifact: model/landscape + targets: + kernel/itc-core: + - conforms_to +- artifact: model/network + targets: + kernel/itc-core: + - conforms_to + model/security: + - uses +- artifact: model/observability + targets: + kernel/itc-core: + - conforms_to + model/task: + - uses +- artifact: model/organization + targets: + kernel/itc-core: + - conforms_to +- artifact: model/security + targets: + kernel/itc-core: + - conforms_to + model/access-control: + - uses +- artifact: model/task + targets: + kernel/itc-core: + - conforms_to +- artifact: standard/caring + targets: + kernel/itc-core: + - conforms_to + model/access-control: + - imports + model/data: + - imports + model/devsecops: + - imports + model/governance: + - imports + model/network: + - imports + model/observability: + - imports + model/organization: + - imports + model/security: + - imports + model/task: + - imports + standard/tagging: + - imports +- artifact: standard/tagging + targets: + kernel/itc-core: + - conforms_to + model/task: + - imports diff --git a/infospace/infospace.yaml b/infospace/infospace.yaml index dabbd7a..bd1f699 100644 --- a/infospace/infospace.yaml +++ b/infospace/infospace.yaml @@ -35,7 +35,15 @@ disciplines: path: standards/tagging/InfoTechCanonTaggingStandard.md - name: CARING Access Governance Standard path: standards/caring/InfoTechCanonCaringAccessGovernanceStandard.md -schemas: {} +schemas: + standard: schemas/standard.schema.yaml + concept: schemas/concept.schema.yaml + mapping: schemas/mapping.schema.yaml + profile: schemas/profile.schema.yaml + assimilation: schemas/assimilation.schema.yaml + interface-card: schemas/interface-card.schema.yaml + agent-brief: schemas/agent-brief.schema.yaml + workplan: schemas/workplan.schema.yaml workflows: [] viability: redundancy_ratio: diff --git a/infospace/schemas/agent-brief.schema.yaml b/infospace/schemas/agent-brief.schema.yaml new file mode 100644 index 0000000..14acf1f --- /dev/null +++ b/infospace/schemas/agent-brief.schema.yaml @@ -0,0 +1,27 @@ +$schema: https://json-schema.org/draft/2020-12/schema +$id: https://info-tech-canon.local/schemas/agent-brief.schema.yaml +title: InfoTechCanon Agent Brief +type: object +required: + - id + - title + - audience + - purpose +properties: + id: + type: string + title: + type: string + audience: + type: string + purpose: + type: string + commands: + type: array + items: + type: string + entrypoints: + type: array + items: + type: string +additionalProperties: true diff --git a/infospace/schemas/assimilation.schema.yaml b/infospace/schemas/assimilation.schema.yaml new file mode 100644 index 0000000..4ca59b5 --- /dev/null +++ b/infospace/schemas/assimilation.schema.yaml @@ -0,0 +1,28 @@ +$schema: https://json-schema.org/draft/2020-12/schema +$id: https://info-tech-canon.local/schemas/assimilation.schema.yaml +title: InfoTechCanon Assimilation Record +type: object +required: + - id + - title + - source + - disposition +properties: + id: + type: string + title: + type: string + source: + type: string + disposition: + enum: + - observe + - map + - adopt + - adapt + - reject + impacts: + type: array + items: + type: string +additionalProperties: true diff --git a/infospace/schemas/concept.schema.yaml b/infospace/schemas/concept.schema.yaml new file mode 100644 index 0000000..85d8404 --- /dev/null +++ b/infospace/schemas/concept.schema.yaml @@ -0,0 +1,26 @@ +$schema: https://json-schema.org/draft/2020-12/schema +$id: https://info-tech-canon.local/schemas/concept.schema.yaml +title: InfoTechCanon Concept +type: object +required: + - id + - title + - owner +properties: + id: + type: string + title: + type: string + owner: + type: string + definition: + type: string + aliases: + type: array + items: + type: string + relationships: + type: array + items: + type: object +additionalProperties: true diff --git a/infospace/schemas/index.yaml b/infospace/schemas/index.yaml new file mode 100644 index 0000000..34bfe44 --- /dev/null +++ b/infospace/schemas/index.yaml @@ -0,0 +1,17 @@ +schemas: + - id: standard + path: standard.schema.yaml + - id: concept + path: concept.schema.yaml + - id: mapping + path: mapping.schema.yaml + - id: profile + path: profile.schema.yaml + - id: assimilation + path: assimilation.schema.yaml + - id: interface-card + path: interface-card.schema.yaml + - id: agent-brief + path: agent-brief.schema.yaml + - id: workplan + path: workplan.schema.yaml diff --git a/infospace/schemas/interface-card.schema.yaml b/infospace/schemas/interface-card.schema.yaml new file mode 100644 index 0000000..e8f5f14 --- /dev/null +++ b/infospace/schemas/interface-card.schema.yaml @@ -0,0 +1,25 @@ +$schema: https://json-schema.org/draft/2020-12/schema +$id: https://info-tech-canon.local/schemas/interface-card.schema.yaml +title: Canon Interface Card +type: object +required: + - id + - title + - consumer + - canon_surfaces +properties: + id: + type: string + title: + type: string + consumer: + type: string + canon_surfaces: + type: array + items: + type: string + expectations: + type: array + items: + type: string +additionalProperties: true diff --git a/infospace/schemas/mapping.schema.yaml b/infospace/schemas/mapping.schema.yaml new file mode 100644 index 0000000..b896543 --- /dev/null +++ b/infospace/schemas/mapping.schema.yaml @@ -0,0 +1,23 @@ +$schema: https://json-schema.org/draft/2020-12/schema +$id: https://info-tech-canon.local/schemas/mapping.schema.yaml +title: InfoTechCanon Mapping +type: object +required: + - id + - title + - source + - target +properties: + id: + type: string + title: + type: string + source: + type: string + target: + type: string + mapping_type: + type: string + confidence: + type: string +additionalProperties: true diff --git a/infospace/schemas/profile.schema.yaml b/infospace/schemas/profile.schema.yaml new file mode 100644 index 0000000..608812d --- /dev/null +++ b/infospace/schemas/profile.schema.yaml @@ -0,0 +1,24 @@ +$schema: https://json-schema.org/draft/2020-12/schema +$id: https://info-tech-canon.local/schemas/profile.schema.yaml +title: InfoTechCanon Profile +type: object +required: + - id + - title + - scope +properties: + id: + type: string + title: + type: string + scope: + type: string + includes: + type: array + items: + type: string + constraints: + type: array + items: + type: object +additionalProperties: true diff --git a/infospace/schemas/standard.schema.yaml b/infospace/schemas/standard.schema.yaml new file mode 100644 index 0000000..31e16b4 --- /dev/null +++ b/infospace/schemas/standard.schema.yaml @@ -0,0 +1,33 @@ +$schema: https://json-schema.org/draft/2020-12/schema +$id: https://info-tech-canon.local/schemas/standard.schema.yaml +title: InfoTechCanon Standard +type: object +required: + - id + - title + - type + - status +properties: + id: + type: string + title: + type: string + type: + enum: + - standard + - specialized-standard + - model + - kernel + status: + type: string + version: + type: string + imports: + type: array + items: + type: string + owned_concepts: + type: array + items: + type: string +additionalProperties: true diff --git a/infospace/schemas/workplan.schema.yaml b/infospace/schemas/workplan.schema.yaml new file mode 100644 index 0000000..2ec15b6 --- /dev/null +++ b/infospace/schemas/workplan.schema.yaml @@ -0,0 +1,38 @@ +$schema: https://json-schema.org/draft/2020-12/schema +$id: https://info-tech-canon.local/schemas/workplan.schema.yaml +title: State Hub Workplan +type: object +required: + - id + - type + - title + - domain + - repo + - status +properties: + id: + type: string + type: + const: workplan + title: + type: string + domain: + type: string + repo: + type: string + status: + enum: + - proposed + - ready + - active + - blocked + - backlog + - finished + - archived + depends_on_workplans: + type: array + items: + type: string + state_hub_workstream_id: + type: string +additionalProperties: true diff --git a/infospace/validation/latest.json b/infospace/validation/latest.json new file mode 100644 index 0000000..eb4ece7 --- /dev/null +++ b/infospace/validation/latest.json @@ -0,0 +1,41 @@ +{ + "details": { + "artifact_count": 15, + "relationship_count": 45 + }, + "errors": [], + "metrics": { + "coherence_components": 1.0, + "consistency_cycles": 0.0, + "coverage_ratio": 1.0, + "granularity_entropy": 1.103307408607834, + "redundancy_ratio": 0.0 + }, + "ok": true, + "warnings": [ + { + "code": "missing_optional_concepts_dir", + "path": "infospace/concepts" + }, + { + "code": "empty_optional_collection", + "path": "infospace/profiles" + }, + { + "code": "empty_optional_collection", + "path": "infospace/patterns" + }, + { + "code": "empty_optional_collection", + "path": "infospace/mappings" + }, + { + "code": "empty_optional_collection", + "path": "infospace/assimilation" + }, + { + "code": "empty_optional_collection", + "path": "infospace/examples" + } + ] +} diff --git a/infospace/views/by-concept.md b/infospace/views/by-concept.md new file mode 100644 index 0000000..daa73e6 --- /dev/null +++ b/infospace/views/by-concept.md @@ -0,0 +1,46 @@ + + +# By Concept + +Concept count: **30** + +| Concept | Owner | Source | +| --- | --- | --- | +| InfoTechCanon Core | `kernel/itc-core` | `artifact_title` | +| InfoTechCanon Kernel Map | `kernel/itc-kernel-map` | `artifact_title` | +| InfoTechCanon Access Control Model | `model/access-control` | `artifact_title` | +| InfoTechCanon Data Model | `model/data` | `artifact_title` | +| InfoTechCanon DevSecOps Model | `model/devsecops` | `artifact_title` | +| InfoTechCanon Governance Model | `model/governance` | `artifact_title` | +| InfoTechCanon Information Space Model | `model/information-space` | `artifact_title` | +| InfoTechCanon Landscape Model | `model/landscape` | `artifact_title` | +| InfoTechCanon Network Model | `model/network` | `artifact_title` | +| InfoTechCanon Observability Model | `model/observability` | `artifact_title` | +| InfoTechCanon Organization Model | `model/organization` | `artifact_title` | +| InfoTechCanon Security Model | `model/security` | `artifact_title` | +| InfoTechCanon Task Model | `model/task` | `artifact_title` | +| InfoTechCanon CARING Access Governance Standard | `standard/caring` | `artifact_title` | +| CARINGAccessDescriptor | `standard/caring` | `frontmatter.owned_concepts` | +| CARINGCanonicalRole | `standard/caring` | `frontmatter.owned_concepts` | +| CARINGOrganizationRelation | `standard/caring` | `frontmatter.owned_concepts` | +| CARINGPlane | `standard/caring` | `frontmatter.owned_concepts` | +| CARINGCapabilityProfile | `standard/caring` | `frontmatter.owned_concepts` | +| CARINGExposureMode | `standard/caring` | `frontmatter.owned_concepts` | +| CARINGExposureEvent | `standard/caring` | `frontmatter.owned_concepts` | +| CARINGDeclaredAccessMap | `standard/caring` | `frontmatter.owned_concepts` | +| CARINGEffectiveAccessMap | `standard/caring` | `frontmatter.owned_concepts` | +| CARINGDerivedCapability | `standard/caring` | `frontmatter.owned_concepts` | +| CARINGInducedAccess | `standard/caring` | `frontmatter.owned_concepts` | +| CARINGRestrictionPrecedence | `standard/caring` | `frontmatter.owned_concepts` | +| CARINGAnalysisFitnessTest | `standard/caring` | `frontmatter.owned_concepts` | +| CARINGAnalysisProcedure | `standard/caring` | `frontmatter.owned_concepts` | +| CARINGRedesignProcedure | `standard/caring` | `frontmatter.owned_concepts` | +| InfoTechCanon Tagging Standard | `standard/tagging` | `artifact_title` | + +## Duplicate Candidates + +No duplicate concept candidates detected. + +## Ownership Conflicts + +No ownership conflicts detected. diff --git a/infospace/views/by-mapping-target.md b/infospace/views/by-mapping-target.md new file mode 100644 index 0000000..5c4f9a6 --- /dev/null +++ b/infospace/views/by-mapping-target.md @@ -0,0 +1,90 @@ + + +# By Mapping Target + +## `kernel/itc-core` + +- `kernel/itc-kernel-map` via `maps` +- `model/access-control` via `conforms_to` +- `model/data` via `conforms_to` +- `model/devsecops` via `conforms_to` +- `model/governance` via `conforms_to` +- `model/information-space` via `conforms_to` +- `model/landscape` via `conforms_to` +- `model/network` via `conforms_to` +- `model/observability` via `conforms_to` +- `model/organization` via `conforms_to` +- `model/security` via `conforms_to` +- `model/task` via `conforms_to` +- `standard/caring` via `conforms_to` +- `standard/tagging` via `conforms_to` + +## `model/access-control` + +- `kernel/itc-kernel-map` via `maps` +- `model/security` via `uses` +- `standard/caring` via `imports` + +## `model/data` + +- `kernel/itc-kernel-map` via `maps` +- `standard/caring` via `imports` + +## `model/devsecops` + +- `kernel/itc-kernel-map` via `maps` +- `standard/caring` via `imports` + +## `model/governance` + +- `kernel/itc-kernel-map` via `maps` +- `model/access-control` via `uses` +- `model/data` via `uses` +- `standard/caring` via `imports` + +## `model/information-space` + +- `kernel/itc-kernel-map` via `maps` + +## `model/landscape` + +- `kernel/itc-kernel-map` via `maps` + +## `model/network` + +- `kernel/itc-kernel-map` via `maps` +- `standard/caring` via `imports` + +## `model/observability` + +- `kernel/itc-kernel-map` via `maps` +- `standard/caring` via `imports` + +## `model/organization` + +- `kernel/itc-kernel-map` via `maps` +- `model/access-control` via `uses` +- `standard/caring` via `imports` + +## `model/security` + +- `kernel/itc-kernel-map` via `maps` +- `model/devsecops` via `uses` +- `model/network` via `uses` +- `standard/caring` via `imports` + +## `model/task` + +- `kernel/itc-kernel-map` via `maps` +- `model/observability` via `uses` +- `standard/caring` via `imports` +- `standard/tagging` via `imports` + +## `standard/caring` + +- `kernel/itc-kernel-map` via `maps` + +## `standard/tagging` + +- `kernel/itc-kernel-map` via `maps` +- `standard/caring` via `imports` diff --git a/infospace/views/by-profile.md b/infospace/views/by-profile.md new file mode 100644 index 0000000..1de074d --- /dev/null +++ b/infospace/views/by-profile.md @@ -0,0 +1,5 @@ + + +# By Profile + +No profiles have been registered yet. diff --git a/infospace/views/by-standard.md b/infospace/views/by-standard.md new file mode 100644 index 0000000..6d39281 --- /dev/null +++ b/infospace/views/by-standard.md @@ -0,0 +1,31 @@ + + +# By Standard + +## InfoTechCanon Core + +- ID: `kernel/itc-core` +- Kind: `kernel` +- Path: `kernel/InfoTechCanonCore.md` +- Relationships: 0 + +## InfoTechCanon Kernel Map + +- ID: `kernel/itc-kernel-map` +- Kind: `kernel` +- Path: `kernel/InfoTechCanonKernelMap.md` +- Relationships: 14 + +## InfoTechCanon CARING Access Governance Standard + +- ID: `standard/caring` +- Kind: `standard` +- Path: `standards/caring/InfoTechCanonCaringAccessGovernanceStandard.md` +- Relationships: 11 + +## InfoTechCanon Tagging Standard + +- ID: `standard/tagging` +- Kind: `standard` +- Path: `standards/tagging/InfoTechCanonTaggingStandard.md` +- Relationships: 2 diff --git a/infospace/views/import-matrix.md b/infospace/views/import-matrix.md new file mode 100644 index 0000000..f7baa0f --- /dev/null +++ b/infospace/views/import-matrix.md @@ -0,0 +1,21 @@ + + +# Import Matrix + +| Artifact | `kernel/itc-core` | `kernel/itc-kernel-map` | `model/access-control` | `model/data` | `model/devsecops` | `model/governance` | `model/information-space` | `model/landscape` | `model/network` | `model/observability` | `model/organization` | `model/security` | `model/task` | `standard/caring` | `standard/tagging` | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| `kernel/itc-core` | | | | | | | | | | | | | | | | +| `kernel/itc-kernel-map` | `maps` | | `maps` | `maps` | `maps` | `maps` | `maps` | `maps` | `maps` | `maps` | `maps` | `maps` | `maps` | `maps` | `maps` | +| `model/access-control` | `conforms_to` | | | | | `uses` | | | | | `uses` | | | | | +| `model/data` | `conforms_to` | | | | | `uses` | | | | | | | | | | +| `model/devsecops` | `conforms_to` | | | | | | | | | | | `uses` | | | | +| `model/governance` | `conforms_to` | | | | | | | | | | | | | | | +| `model/information-space` | `conforms_to` | | | | | | | | | | | | | | | +| `model/landscape` | `conforms_to` | | | | | | | | | | | | | | | +| `model/network` | `conforms_to` | | | | | | | | | | | `uses` | | | | +| `model/observability` | `conforms_to` | | | | | | | | | | | | `uses` | | | +| `model/organization` | `conforms_to` | | | | | | | | | | | | | | | +| `model/security` | `conforms_to` | | `uses` | | | | | | | | | | | | | +| `model/task` | `conforms_to` | | | | | | | | | | | | | | | +| `standard/caring` | `conforms_to` | | `imports` | `imports` | `imports` | `imports` | | | `imports` | `imports` | `imports` | `imports` | `imports` | | `imports` | +| `standard/tagging` | `conforms_to` | | | | | | | | | | | | `imports` | | | diff --git a/infospace/views/kernel-overview.md b/infospace/views/kernel-overview.md new file mode 100644 index 0000000..017d272 --- /dev/null +++ b/infospace/views/kernel-overview.md @@ -0,0 +1,19 @@ + + +# Kernel Overview + +- Infospace: `canon` +- Artifacts: 15 + +## Artifact Kinds + +- `kernel`: 2 +- `model`: 11 +- `standard`: 2 + +## Relationship Types + +- `conforms_to`: 13 +- `imports`: 11 +- `maps`: 14 +- `uses`: 7 diff --git a/infospace/views/repository-tree.md b/infospace/views/repository-tree.md new file mode 100644 index 0000000..b9ba248 --- /dev/null +++ b/infospace/views/repository-tree.md @@ -0,0 +1,56 @@ + + +# Repository Tree + +File count: **50** + +- `README.md` +- `agent/README.md` +- `agent/global-agent-brief.md` +- `artifacts/index.yaml` +- `assimilation/README.md` +- `examples/README.md` +- `indexes/README.md` +- `indexes/artifact-tree.yaml` +- `indexes/concept-ownership.yaml` +- `indexes/import-matrix.yaml` +- `infospace.yaml` +- `kernel/InfoTechCanonCore.md` +- `kernel/InfoTechCanonKernelMap.md` +- `mappings/README.md` +- `models/access-control/InfoTechCanonAccessControlModel.md` +- `models/data/InfoTechCanonDataModel.md` +- `models/devsecops/InfoTechCanonDevSecOpsModel.md` +- `models/governance/InfoTechCanonGovernanceModel.md` +- `models/information-space/InfoTechCanonInformationSpaceModel.md` +- `models/landscape/InfoTechCanonLandscapeModel.md` +- `models/network/InfoTechCanonNetworkModel.md` +- `models/observability/InfoTechCanonObservabilityModel.md` +- `models/organization/InfoTechCanonOrganizationModel.md` +- `models/security/InfoTechCanonSecurityModel.md` +- `models/task/InfoTechCanonTaskModel.md` +- `patterns/README.md` +- `profiles/README.md` +- `reports/scaffold-placement.md` +- `schemas/README.md` +- `schemas/agent-brief.schema.yaml` +- `schemas/assimilation.schema.yaml` +- `schemas/concept.schema.yaml` +- `schemas/index.yaml` +- `schemas/interface-card.schema.yaml` +- `schemas/mapping.schema.yaml` +- `schemas/profile.schema.yaml` +- `schemas/standard.schema.yaml` +- `schemas/workplan.schema.yaml` +- `standards/caring/InfoTechCanonCaringAccessGovernanceStandard.md` +- `standards/tagging/InfoTechCanonTaggingStandard.md` +- `validation/README.md` +- `validation/latest.json` +- `views/README.md` +- `views/by-concept.md` +- `views/by-mapping-target.md` +- `views/by-profile.md` +- `views/by-standard.md` +- `views/import-matrix.md` +- `views/kernel-overview.md` +- `views/repository-tree.md` diff --git a/src/info_tech_canon/__init__.py b/src/info_tech_canon/__init__.py index d5f78ec..f2727b7 100644 --- a/src/info_tech_canon/__init__.py +++ b/src/info_tech_canon/__init__.py @@ -3,21 +3,33 @@ from .service import ( CanonServiceError, artifact_graph, + generate_agent_briefs, + generate_indexes, + generate_tree, inspect_canon, list_artifacts, list_models, list_standards, + list_views, profile_inspect, + read_view, validate_canon, + write_validation_report, ) __all__ = [ "CanonServiceError", "artifact_graph", + "generate_agent_briefs", + "generate_indexes", + "generate_tree", "inspect_canon", "list_artifacts", "list_models", "list_standards", + "list_views", "profile_inspect", + "read_view", "validate_canon", + "write_validation_report", ] diff --git a/src/info_tech_canon/api.py b/src/info_tech_canon/api.py index df5e717..20f08e4 100644 --- a/src/info_tech_canon/api.py +++ b/src/info_tech_canon/api.py @@ -14,7 +14,9 @@ from .service import ( list_artifacts, list_models, list_standards, + list_views, profile_inspect, + read_view, validate_canon, ) @@ -86,6 +88,11 @@ def _route( if path == "/graph": graph_format = _first(query, "format") or "json" return HTTPStatus.OK, artifact_graph(root, output_format=graph_format) + if path == "/views": + return HTTPStatus.OK, list_views(root) + if path.startswith("/views/"): + name = path.removeprefix("/views/").strip("/") + return HTTPStatus.OK, read_view(name, root) if path.startswith("/profiles/") and path.endswith("/inspect"): profile = path.removeprefix("/profiles/").removesuffix("/inspect").strip("/") return HTTPStatus.OK, profile_inspect(profile, root) diff --git a/src/info_tech_canon/cli.py b/src/info_tech_canon/cli.py index ad53b7c..38c92ba 100644 --- a/src/info_tech_canon/cli.py +++ b/src/info_tech_canon/cli.py @@ -11,12 +11,18 @@ from .api import serve from .service import ( CanonServiceError, artifact_graph, + generate_agent_briefs, + generate_indexes, + generate_tree, inspect_canon, list_artifacts, list_models, list_standards, + list_views, profile_inspect, + read_view, validate_canon, + write_validation_report, ) @@ -46,8 +52,26 @@ def build_parser() -> argparse.ArgumentParser: standards.set_defaults(handler=_standards) validate = sub.add_parser("validate", help="Validate the canon infospace") + validate.add_argument( + "--write", + default="", + help="Write the JSON validation payload to this path.", + ) validate.set_defaults(handler=_validate) + index = sub.add_parser("index", help="Refresh generated indexes and views") + index.set_defaults(handler=_index) + + tree = sub.add_parser("tree", help="Refresh the generated infospace tree") + tree.set_defaults(handler=_tree) + + agent_briefs = sub.add_parser("agent-briefs", help="Refresh generated agent briefs") + agent_briefs.set_defaults(handler=_agent_briefs) + + views = sub.add_parser("views", help="List or read generated views") + views.add_argument("name", nargs="?", default="") + views.set_defaults(handler=_views) + graph = sub.add_parser("graph", help="Export the canon artifact graph") graph.add_argument("--format", choices=["json", "mermaid"], default="json") graph.set_defaults(handler=_graph) @@ -113,9 +137,29 @@ def _standards(args: argparse.Namespace) -> dict[str, Any]: def _validate(args: argparse.Namespace) -> dict[str, Any]: + if args.write: + return write_validation_report(args.write, _root(args)) return validate_canon(_root(args)) +def _index(args: argparse.Namespace) -> dict[str, Any]: + return generate_indexes(_root(args)) + + +def _tree(args: argparse.Namespace) -> dict[str, Any]: + return generate_tree(_root(args)) + + +def _agent_briefs(args: argparse.Namespace) -> dict[str, Any]: + return generate_agent_briefs(_root(args)) + + +def _views(args: argparse.Namespace) -> dict[str, Any]: + if args.name: + return read_view(args.name, _root(args)) + return list_views(_root(args)) + + def _graph(args: argparse.Namespace) -> dict[str, Any]: return artifact_graph(_root(args), output_format=args.format) diff --git a/src/info_tech_canon/generation.py b/src/info_tech_canon/generation.py new file mode 100644 index 0000000..22c1335 --- /dev/null +++ b/src/info_tech_canon/generation.py @@ -0,0 +1,427 @@ +from __future__ import annotations + +from collections import defaultdict +from pathlib import Path +from typing import Any + +import yaml + + +GENERATED_NOTICE = "" + + +def generate_indexes(context: Any) -> dict[str, Any]: + assets: list[dict[str, Any]] = [] + ownership = concept_ownership(context) + import_matrix = relationship_matrix(context) + + assets.append( + _write_yaml( + context.infospace_root / "indexes" / "concept-ownership.yaml", + ownership, + ) + ) + assets.append( + _write_yaml( + context.infospace_root / "indexes" / "import-matrix.yaml", + import_matrix, + ) + ) + assets.append( + _write_yaml( + context.infospace_root / "indexes" / "artifact-tree.yaml", + artifact_tree(context), + ) + ) + assets.extend(generate_views(context, ownership, import_matrix)["files"]) + return _result("index", assets) + + +def generate_views( + context: Any, + ownership: dict[str, Any] | None = None, + import_matrix: dict[str, Any] | None = None, +) -> dict[str, Any]: + ownership = ownership or concept_ownership(context) + import_matrix = import_matrix or relationship_matrix(context) + files = [ + _write_text( + context.infospace_root / "views" / "by-standard.md", + _render_by_standard(context), + ), + _write_text( + context.infospace_root / "views" / "by-concept.md", + _render_by_concept(ownership), + ), + _write_text( + context.infospace_root / "views" / "by-profile.md", + _render_by_profile(context), + ), + _write_text( + context.infospace_root / "views" / "by-mapping-target.md", + _render_by_mapping_target(context), + ), + _write_text( + context.infospace_root / "views" / "kernel-overview.md", + _render_kernel_overview(context), + ), + _write_text( + context.infospace_root / "views" / "import-matrix.md", + _render_import_matrix(import_matrix), + ), + ] + return _result("views", files) + + +def generate_tree(context: Any) -> dict[str, Any]: + tree = artifact_tree(context) + files = [ + _write_yaml(context.infospace_root / "indexes" / "artifact-tree.yaml", tree), + _write_text( + context.infospace_root / "views" / "repository-tree.md", + _render_repository_tree(tree), + ), + ] + return _result("tree", files) + + +def generate_agent_briefs(context: Any) -> dict[str, Any]: + files = [ + _write_text( + context.infospace_root / "agent" / "global-agent-brief.md", + _render_global_agent_brief(context), + ) + ] + return _result("agent-briefs", files) + + +def list_generated_views(context: Any) -> dict[str, Any]: + views = [] + for path in sorted((context.infospace_root / "views").glob("*.md")): + views.append( + { + "name": path.name, + "path": str(path.relative_to(context.infospace_root)), + "generated": _is_generated(path), + } + ) + return {"ok": True, "count": len(views), "views": views} + + +def read_generated_view(context: Any, name: str) -> dict[str, Any]: + if "/" in name or "\\" in name: + raise ValueError("View name must be a single file name.") + path = context.infospace_root / "views" / name + if not path.is_file(): + raise FileNotFoundError(name) + return { + "ok": True, + "name": name, + "path": str(path.relative_to(context.infospace_root)), + "generated": _is_generated(path), + "content": path.read_text(encoding="utf-8"), + } + + +def concept_ownership(context: Any) -> dict[str, Any]: + concepts: list[dict[str, Any]] = [] + for artifact in sorted(context.infospace.artifacts, key=lambda item: item.id): + concepts.append( + { + "concept": artifact.title, + "owner": artifact.id, + "path": artifact.path, + "source": "artifact_title", + } + ) + frontmatter = _frontmatter(context.infospace_root / artifact.path) + owned = frontmatter.get("owned_concepts") or [] + if isinstance(owned, list): + for concept in owned: + concepts.append( + { + "concept": str(concept), + "owner": artifact.id, + "path": artifact.path, + "source": "frontmatter.owned_concepts", + } + ) + + by_key: dict[str, list[dict[str, Any]]] = defaultdict(list) + for item in concepts: + by_key[_normalize_concept(item["concept"])].append(item) + + duplicates = [ + {"normalized": key, "candidates": items} + for key, items in sorted(by_key.items()) + if len(items) > 1 + ] + conflicts = [ + { + "normalized": item["normalized"], + "owners": sorted({candidate["owner"] for candidate in item["candidates"]}), + "candidates": item["candidates"], + } + for item in duplicates + if len({candidate["owner"] for candidate in item["candidates"]}) > 1 + ] + return { + "concept_count": len(concepts), + "concepts": concepts, + "duplicate_candidates": duplicates, + "ownership_conflicts": conflicts, + } + + +def relationship_matrix(context: Any) -> dict[str, Any]: + artifact_ids = sorted(artifact.id for artifact in context.infospace.artifacts) + rows: list[dict[str, Any]] = [] + for artifact in sorted(context.infospace.artifacts, key=lambda item: item.id): + targets: dict[str, list[str]] = {target: [] for target in artifact_ids} + for relationship in artifact.relationships: + target = relationship.get("target") + relation_type = str(relationship.get("type") or "related") + if isinstance(target, str) and target in targets: + targets[target].append(relation_type) + rows.append( + { + "artifact": artifact.id, + "targets": { + target: sorted(types) + for target, types in targets.items() + if types + }, + } + ) + return {"artifacts": artifact_ids, "rows": rows} + + +def artifact_tree(context: Any) -> dict[str, Any]: + files: list[dict[str, Any]] = [] + for path in sorted(context.infospace_root.rglob("*")): + if path.is_file(): + relative = path.relative_to(context.infospace_root) + files.append( + { + "path": str(relative), + "directory": str(relative.parent), + "name": path.name, + } + ) + return {"root": "infospace", "file_count": len(files), "files": files} + + +def _render_by_standard(context: Any) -> str: + lines = _heading("By Standard") + standards = [ + artifact + for artifact in context.infospace.artifacts + if artifact.kind in {"kernel", "standard"} + ] + for artifact in sorted(standards, key=lambda item: item.id): + lines.extend( + [ + f"## {artifact.title}", + "", + f"- ID: `{artifact.id}`", + f"- Kind: `{artifact.kind}`", + f"- Path: `{artifact.path}`", + f"- Relationships: {len(artifact.relationships)}", + "", + ] + ) + return "\n".join(lines).rstrip() + "\n" + + +def _render_by_concept(ownership: dict[str, Any]) -> str: + lines = _heading("By Concept") + lines.extend( + [ + f"Concept count: **{ownership['concept_count']}**", + "", + "| Concept | Owner | Source |", + "| --- | --- | --- |", + ] + ) + for concept in ownership["concepts"]: + lines.append( + f"| {concept['concept']} | `{concept['owner']}` | `{concept['source']}` |" + ) + lines.extend(["", "## Duplicate Candidates", ""]) + duplicates = ownership["duplicate_candidates"] + if not duplicates: + lines.append("No duplicate concept candidates detected.") + else: + for duplicate in duplicates: + lines.append(f"- `{duplicate['normalized']}`") + lines.extend(["", "## Ownership Conflicts", ""]) + conflicts = ownership["ownership_conflicts"] + if not conflicts: + lines.append("No ownership conflicts detected.") + else: + for conflict in conflicts: + owners = ", ".join(f"`{owner}`" for owner in conflict["owners"]) + lines.append(f"- `{conflict['normalized']}` owned by {owners}") + return "\n".join(lines).rstrip() + "\n" + + +def _render_by_profile(context: Any) -> str: + lines = _heading("By Profile") + profiles = sorted((context.infospace_root / "profiles").glob("*/profile.yaml")) + if not profiles: + lines.append("No profiles have been registered yet.") + for path in profiles: + lines.extend( + [ + f"## {path.parent.name}", + "", + f"- Path: `{path.relative_to(context.infospace_root)}`", + "", + ] + ) + return "\n".join(lines).rstrip() + "\n" + + +def _render_by_mapping_target(context: Any) -> str: + incoming: dict[str, list[tuple[str, str]]] = defaultdict(list) + for artifact in context.infospace.artifacts: + for relationship in artifact.relationships: + target = relationship.get("target") + relation_type = str(relationship.get("type") or "related") + if isinstance(target, str): + incoming[target].append((artifact.id, relation_type)) + lines = _heading("By Mapping Target") + for target in sorted(incoming): + lines.extend([f"## `{target}`", ""]) + for source, relation_type in sorted(incoming[target]): + lines.append(f"- `{source}` via `{relation_type}`") + lines.append("") + return "\n".join(lines).rstrip() + "\n" + + +def _render_kernel_overview(context: Any) -> str: + kind_counts: dict[str, int] = defaultdict(int) + relationship_counts: dict[str, int] = defaultdict(int) + for artifact in context.infospace.artifacts: + kind_counts[artifact.kind] += 1 + for relationship in artifact.relationships: + relationship_counts[str(relationship.get("type") or "related")] += 1 + lines = _heading("Kernel Overview") + lines.extend( + [ + f"- Infospace: `{context.infospace.config.slug}`", + f"- Artifacts: {len(context.infospace.artifacts)}", + "", + "## Artifact Kinds", + "", + ] + ) + for kind, count in sorted(kind_counts.items()): + lines.append(f"- `{kind}`: {count}") + lines.extend(["", "## Relationship Types", ""]) + for relation_type, count in sorted(relationship_counts.items()): + lines.append(f"- `{relation_type}`: {count}") + return "\n".join(lines).rstrip() + "\n" + + +def _render_import_matrix(matrix: dict[str, Any]) -> str: + artifacts = matrix["artifacts"] + lines = _heading("Import Matrix") + header = "| Artifact | " + " | ".join(f"`{artifact}`" for artifact in artifacts) + " |" + divider = "| --- | " + " | ".join("---" for _ in artifacts) + " |" + lines.extend([header, divider]) + for row in matrix["rows"]: + cells = [] + targets = row["targets"] + for artifact in artifacts: + cells.append(", ".join(f"`{item}`" for item in targets.get(artifact, []))) + lines.append(f"| `{row['artifact']}` | " + " | ".join(cells) + " |") + return "\n".join(lines).rstrip() + "\n" + + +def _render_repository_tree(tree: dict[str, Any]) -> str: + lines = _heading("Repository Tree") + lines.append(f"File count: **{tree['file_count']}**") + lines.append("") + for file_info in tree["files"]: + lines.append(f"- `{file_info['path']}`") + return "\n".join(lines).rstrip() + "\n" + + +def _render_global_agent_brief(context: Any) -> str: + lines = _heading("Global Agent Brief") + lines.extend( + [ + "This brief summarizes the current canon service surface for agents.", + "", + f"- Infospace slug: `{context.infospace.config.slug}`", + f"- Artifact count: {len(context.infospace.artifacts)}", + "- Primary confidence command: `make validate`", + "- Refresh generated indexes and views with: `make index`", + "", + "## Useful Commands", + "", + "- `PYTHONPATH=src python3 -m info_tech_canon inspect`", + "- `PYTHONPATH=src python3 -m info_tech_canon validate`", + "- `PYTHONPATH=src python3 -m info_tech_canon graph`", + "- `PYTHONPATH=src python3 -m info_tech_canon index`", + "", + "## Consumption Notes", + "", + "- Treat `seeds/` as provenance.", + "- Treat `infospace/` as the service-consumable canon root.", + "- Generated files are marked and can be refreshed deterministically.", + ] + ) + return "\n".join(lines).rstrip() + "\n" + + +def _heading(title: str) -> list[str]: + return [GENERATED_NOTICE, "", f"# {title}", ""] + + +def _write_text(path: Path, content: str) -> dict[str, Any]: + path.parent.mkdir(parents=True, exist_ok=True) + old = path.read_text(encoding="utf-8") if path.exists() else None + changed = old != content + if changed: + path.write_text(content, encoding="utf-8") + return {"path": str(path), "changed": changed} + + +def _write_yaml(path: Path, data: dict[str, Any]) -> dict[str, Any]: + path.parent.mkdir(parents=True, exist_ok=True) + content = yaml.safe_dump(data, sort_keys=False) + return _write_text(path, content) + + +def _result(kind: str, files: list[dict[str, Any]]) -> dict[str, Any]: + return { + "ok": True, + "kind": kind, + "count": len(files), + "changed": [item for item in files if item["changed"]], + "files": files, + } + + +def _frontmatter(path: Path) -> dict[str, Any]: + text = path.read_text(encoding="utf-8") + if not text.startswith("---\n"): + return {} + end = text.find("\n---\n", 4) + if end == -1: + return {} + data = yaml.safe_load(text[4:end]) or {} + return data if isinstance(data, dict) else {} + + +def _normalize_concept(value: str) -> str: + return "-".join(value.lower().replace("_", "-").split()) + + +def _is_generated(path: Path) -> bool: + try: + return path.read_text(encoding="utf-8").startswith(GENERATED_NOTICE) + except FileNotFoundError: + return False diff --git a/src/info_tech_canon/service.py b/src/info_tech_canon/service.py index be8aba3..e65fb8e 100644 --- a/src/info_tech_canon/service.py +++ b/src/info_tech_canon/service.py @@ -3,10 +3,12 @@ from __future__ import annotations from collections import Counter from dataclasses import asdict, dataclass from pathlib import Path +import json from typing import Any import yaml +from . import generation from .bench import ( Infospace, KnowledgeArtifact, @@ -15,6 +17,7 @@ from .bench import ( relationship_summary, run_collection_checks, ) +from .validation import structural_checks REPO_ROOT = Path(__file__).resolve().parents[2] @@ -121,6 +124,7 @@ def list_standards(root: Path | str | None = None) -> dict[str, Any]: def validate_canon(root: Path | str | None = None) -> dict[str, Any]: context = load_context(root) errors: list[dict[str, Any]] = [] + warnings: list[dict[str, Any]] = [] artifact_ids = {artifact.id for artifact in context.infospace.artifacts} for artifact in context.infospace.artifacts: @@ -161,15 +165,31 @@ def validate_canon(root: Path | str | None = None) -> dict[str, Any]: context.infospace.config.viability, ) errors.extend(threshold_errors) + structural = structural_checks(context) + errors.extend(structural["errors"]) + warnings.extend(structural["warnings"]) return { "ok": not errors, "errors": errors, + "warnings": warnings, "metrics": checks.metrics, "details": checks.details, } +def write_validation_report( + destination: Path | str, + root: Path | str | None = None, +) -> dict[str, Any]: + payload = validate_canon(root) + path = Path(destination) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8") + payload["report_path"] = str(path) + return payload + + def artifact_graph( root: Path | str | None = None, *, @@ -221,6 +241,39 @@ def profile_inspect( return {"ok": True, "profile": data, "path": str(profile_path)} +def generate_indexes(root: Path | str | None = None) -> dict[str, Any]: + return generation.generate_indexes(load_context(root)) + + +def generate_tree(root: Path | str | None = None) -> dict[str, Any]: + return generation.generate_tree(load_context(root)) + + +def generate_agent_briefs(root: Path | str | None = None) -> dict[str, Any]: + return generation.generate_agent_briefs(load_context(root)) + + +def list_views(root: Path | str | None = None) -> dict[str, Any]: + return generation.list_generated_views(load_context(root)) + + +def read_view(name: str, root: Path | str | None = None) -> dict[str, Any]: + try: + return generation.read_generated_view(load_context(root), name) + except FileNotFoundError as exc: + raise CanonServiceError( + "missing_view", + f"View not found: {name}", + {"view": name}, + ) from exc + except ValueError as exc: + raise CanonServiceError( + "invalid_view_name", + str(exc), + {"view": name}, + ) from exc + + def _artifact_to_dict( artifact: KnowledgeArtifact, infospace_root: Path, diff --git a/src/info_tech_canon/validation.py b/src/info_tech_canon/validation.py new file mode 100644 index 0000000..c0f5d1c --- /dev/null +++ b/src/info_tech_canon/validation.py @@ -0,0 +1,360 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any + +import yaml + + +REQUIRED_TOP_LEVEL_FILES = ( + "README.md", + "INTENT.md", + "SCOPE.md", + "canon.yaml", + "pyproject.toml", + "workplans/index.yaml", + "infospace/infospace.yaml", + "infospace/artifacts/index.yaml", +) + +REQUIRED_INFOSPACE_DIRS = ( + "kernel", + "models", + "standards", + "profiles", + "patterns", + "mappings", + "assimilation", + "schemas", + "views", + "agent", + "examples", + "validation", + "indexes", +) + +OPTIONAL_COLLECTION_DIRS = ( + "profiles", + "patterns", + "mappings", + "assimilation", + "examples", +) + +REQUIRED_SCHEMAS = ( + "standard.schema.yaml", + "concept.schema.yaml", + "mapping.schema.yaml", + "profile.schema.yaml", + "assimilation.schema.yaml", + "interface-card.schema.yaml", + "agent-brief.schema.yaml", + "workplan.schema.yaml", +) + + +def structural_checks(context: Any) -> dict[str, list[dict[str, Any]]]: + errors: list[dict[str, Any]] = [] + warnings: list[dict[str, Any]] = [] + + _check_required_top_level_files(context.repo_root, errors) + _check_required_infospace_dirs(context.infospace_root, errors) + _check_required_schemas(context.infospace_root, errors) + _check_canon_paths(context.repo_root, context.infospace_root, errors) + _check_artifact_index(context.repo_root, context.infospace_root, errors) + _check_optional_assets(context.infospace_root, warnings) + + return {"errors": errors, "warnings": warnings} + + +def _check_required_top_level_files( + repo_root: Path, + errors: list[dict[str, Any]], +) -> None: + for relative in REQUIRED_TOP_LEVEL_FILES: + if not (repo_root / relative).is_file(): + errors.append( + { + "code": "missing_required_file", + "path": relative, + } + ) + + +def _check_required_infospace_dirs( + infospace_root: Path, + errors: list[dict[str, Any]], +) -> None: + for relative in REQUIRED_INFOSPACE_DIRS: + if not (infospace_root / relative).is_dir(): + errors.append( + { + "code": "missing_required_infospace_dir", + "path": str(Path("infospace") / relative), + } + ) + + +def _check_required_schemas( + infospace_root: Path, + errors: list[dict[str, Any]], +) -> None: + for filename in REQUIRED_SCHEMAS: + if not (infospace_root / "schemas" / filename).is_file(): + errors.append( + { + "code": "missing_schema", + "path": str(Path("infospace") / "schemas" / filename), + } + ) + + +def _check_canon_paths( + repo_root: Path, + infospace_root: Path, + errors: list[dict[str, Any]], +) -> None: + canon_path = repo_root / "canon.yaml" + canon = _read_yaml(canon_path, errors) + if not isinstance(canon, dict): + return + + indexed_paths = _artifact_paths_by_path(infospace_root, errors) + for section in ("kernel", "models", "standards"): + items = canon.get(section) or [] + if not isinstance(items, list): + errors.append( + { + "code": "invalid_canon_section", + "section": section, + "message": "Expected a list.", + } + ) + continue + for item in items: + if not isinstance(item, dict): + errors.append( + { + "code": "invalid_canon_entry", + "section": section, + "message": "Expected a mapping.", + } + ) + continue + path = str(item.get("path") or "") + if not path: + errors.append( + { + "code": "missing_canon_path", + "section": section, + "id": item.get("id"), + } + ) + continue + if not (repo_root / path).is_file(): + errors.append( + { + "code": "missing_canon_path_target", + "section": section, + "id": item.get("id"), + "path": path, + } + ) + relative_infospace_path = _strip_infospace_prefix(path) + if relative_infospace_path not in indexed_paths: + errors.append( + { + "code": "canon_path_not_indexed", + "section": section, + "id": item.get("id"), + "path": path, + } + ) + + +def _check_artifact_index( + repo_root: Path, + infospace_root: Path, + errors: list[dict[str, Any]], +) -> None: + index_path = infospace_root / "artifacts" / "index.yaml" + index = _read_yaml(index_path, errors) + if not isinstance(index, dict): + return + artifacts = index.get("artifacts") + if not isinstance(artifacts, list): + errors.append( + { + "code": "invalid_artifact_index", + "path": "infospace/artifacts/index.yaml", + "message": "Expected artifacts list.", + } + ) + return + + ids: set[str] = set() + for artifact in artifacts: + if not isinstance(artifact, dict): + errors.append( + { + "code": "invalid_artifact_entry", + "message": "Expected artifact mapping.", + } + ) + continue + artifact_id = str(artifact.get("id") or "") + if not artifact_id: + errors.append({"code": "missing_artifact_id"}) + elif artifact_id in ids: + errors.append( + { + "code": "duplicate_artifact_id", + "artifact_id": artifact_id, + } + ) + ids.add(artifact_id) + + for field in ("path", "kind", "title"): + if not artifact.get(field): + errors.append( + { + "code": "missing_artifact_field", + "artifact_id": artifact_id, + "field": field, + } + ) + + relative_path = str(artifact.get("path") or "") + if relative_path and not (infospace_root / relative_path).is_file(): + errors.append( + { + "code": "missing_artifact_path", + "artifact_id": artifact_id, + "path": relative_path, + } + ) + + provenance = artifact.get("provenance") or {} + if isinstance(provenance, dict): + source_path = provenance.get("source_path") + if isinstance(source_path, str) and source_path: + if not (repo_root / source_path).is_file(): + errors.append( + { + "code": "missing_provenance_source", + "artifact_id": artifact_id, + "source_path": source_path, + } + ) + + for artifact in artifacts: + if not isinstance(artifact, dict): + continue + artifact_id = str(artifact.get("id") or "") + relationships = artifact.get("relationships") or [] + if not isinstance(relationships, list): + errors.append( + { + "code": "invalid_relationships", + "artifact_id": artifact_id, + "message": "Expected relationship list.", + } + ) + continue + for relationship in relationships: + if not isinstance(relationship, dict): + errors.append( + { + "code": "invalid_relationship", + "artifact_id": artifact_id, + } + ) + continue + target = relationship.get("target") + if target not in ids: + errors.append( + { + "code": "missing_relationship_target", + "artifact_id": artifact_id, + "target": target, + } + ) + + +def _check_optional_assets( + infospace_root: Path, + warnings: list[dict[str, Any]], +) -> None: + global_brief = infospace_root / "agent" / "global-agent-brief.md" + if not global_brief.is_file(): + warnings.append( + { + "code": "missing_optional_agent_brief", + "path": "infospace/agent/global-agent-brief.md", + } + ) + + concepts_dir = infospace_root / "concepts" + if not concepts_dir.is_dir(): + warnings.append( + { + "code": "missing_optional_concepts_dir", + "path": "infospace/concepts", + } + ) + + for relative in OPTIONAL_COLLECTION_DIRS: + directory = infospace_root / relative + if directory.is_dir() and not _has_substantive_files(directory): + warnings.append( + { + "code": "empty_optional_collection", + "path": str(Path("infospace") / relative), + } + ) + + +def _artifact_paths_by_path( + infospace_root: Path, + errors: list[dict[str, Any]], +) -> set[str]: + index = _read_yaml(infospace_root / "artifacts" / "index.yaml", errors) + if not isinstance(index, dict): + return set() + artifacts = index.get("artifacts") or [] + if not isinstance(artifacts, list): + return set() + return { + str(artifact.get("path")) + for artifact in artifacts + if isinstance(artifact, dict) and artifact.get("path") + } + + +def _read_yaml(path: Path, errors: list[dict[str, Any]]) -> Any: + try: + with path.open("r", encoding="utf-8") as handle: + return yaml.safe_load(handle) or {} + except FileNotFoundError: + errors.append({"code": "missing_yaml", "path": str(path)}) + except yaml.YAMLError as exc: + errors.append( + { + "code": "invalid_yaml", + "path": str(path), + "message": str(exc), + } + ) + return None + + +def _strip_infospace_prefix(path: str) -> str: + prefix = "infospace/" + return path[len(prefix) :] if path.startswith(prefix) else path + + +def _has_substantive_files(directory: Path) -> bool: + for path in directory.rglob("*"): + if path.is_file() and path.name != "README.md": + return True + return False diff --git a/tests/test_api.py b/tests/test_api.py index 7b7fc66..f230c3f 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,6 +1,8 @@ from http import HTTPStatus +import shutil from info_tech_canon.api import _route +from info_tech_canon.service import DEFAULT_INFOSPACE_ROOT, generate_indexes def test_api_route_inspect() -> None: @@ -24,3 +26,16 @@ def test_api_route_unknown_endpoint() -> None: assert status == HTTPStatus.NOT_FOUND assert payload["ok"] is False assert payload["error"]["code"] == "not_found" + + +def test_api_route_reads_generated_view(tmp_path) -> None: + root = tmp_path / "infospace" + shutil.copytree(DEFAULT_INFOSPACE_ROOT, root) + generate_indexes(root) + + status, payload = _route("/views/by-standard.md", {}, root) + + assert status == HTTPStatus.OK + assert payload["ok"] is True + assert payload["generated"] is True + assert "# By Standard" in payload["content"] diff --git a/tests/test_cli.py b/tests/test_cli.py index 9401500..c3de3d1 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,6 +1,8 @@ import json +import shutil from info_tech_canon.cli import main +from info_tech_canon.service import DEFAULT_INFOSPACE_ROOT def test_cli_inspect_emits_json(capsys) -> None: @@ -19,3 +21,15 @@ def test_cli_missing_profile_uses_structured_error(capsys) -> None: payload = json.loads(capsys.readouterr().out) assert payload["ok"] is False assert payload["error"]["code"] == "missing_profile" + + +def test_cli_index_generates_views(capsys, tmp_path) -> None: + root = tmp_path / "infospace" + shutil.copytree(DEFAULT_INFOSPACE_ROOT, root) + + exit_code = main(["--root", str(root), "index"]) + + assert exit_code == 0 + payload = json.loads(capsys.readouterr().out) + assert payload["ok"] is True + assert (root / "views" / "kernel-overview.md").is_file() diff --git a/tests/test_service.py b/tests/test_service.py index c0e0ff0..d9f71ea 100644 --- a/tests/test_service.py +++ b/tests/test_service.py @@ -1,10 +1,15 @@ from info_tech_canon.service import ( artifact_graph, + generate_agent_briefs, + generate_indexes, + generate_tree, inspect_canon, list_models, list_standards, validate_canon, ) +from info_tech_canon.service import DEFAULT_INFOSPACE_ROOT +import shutil def test_inspect_canon_counts_artifact_kinds() -> None: @@ -30,6 +35,7 @@ def test_validate_canon_passes_scaffold() -> None: assert payload["ok"] is True assert payload["errors"] == [] + assert "warnings" in payload assert payload["details"]["artifact_count"] == 15 @@ -39,3 +45,21 @@ def test_graph_exports_relationship_summary() -> None: assert payload["ok"] is True assert payload["graph"]["node_count"] == 15 assert payload["graph"]["edge_count"] > 15 + + +def test_generators_write_expected_assets(tmp_path) -> None: + root = tmp_path / "infospace" + shutil.copytree(DEFAULT_INFOSPACE_ROOT, root) + + index_payload = generate_indexes(root) + tree_payload = generate_tree(root) + brief_payload = generate_agent_briefs(root) + + assert index_payload["ok"] is True + assert tree_payload["ok"] is True + assert brief_payload["ok"] is True + assert (root / "indexes" / "concept-ownership.yaml").is_file() + assert (root / "views" / "by-standard.md").read_text( + encoding="utf-8" + ).startswith("