diff --git a/README.md b/README.md index 2cf5df0..3fddf68 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,9 @@ 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 profile inspect small-saas +PYTHONPATH=src python3 -m info_tech_canon profile validate small-saas +PYTHONPATH=src python3 -m info_tech_canon profile graph small-saas PYTHONPATH=src python3 -m info_tech_canon api --host 127.0.0.1 --port 8765 ``` @@ -43,6 +46,8 @@ After package installation, the same commands are available through the - `GET /views` - `GET /views/{name}` - `GET /profiles/{profile}/inspect` +- `GET /profiles/{profile}/validate` +- `GET /profiles/{profile}/graph` ## Maintenance @@ -52,3 +57,10 @@ make index make tree make agent-briefs ``` + +## First Profile Proof + +The first executable profile proof is `small-saas`. It lives under +`infospace/profiles/small-saas/` and includes connected example artifacts for a +tenant-aware SaaS service: service, system, tenants, user, team, dataset, +deployment, task, policy, control, evidence, and incident. diff --git a/infospace/agent/global-agent-brief.md b/infospace/agent/global-agent-brief.md index d7e349f..5da9cf0 100644 --- a/infospace/agent/global-agent-brief.md +++ b/infospace/agent/global-agent-brief.md @@ -5,7 +5,7 @@ This brief summarizes the current canon service surface for agents. - Infospace slug: `canon` -- Artifact count: 15 +- Artifact count: 29 - Primary confidence command: `make validate` - Refresh generated indexes and views with: `make index` diff --git a/infospace/artifacts/index.yaml b/infospace/artifacts/index.yaml index 82aab32..bddb6ae 100644 --- a/infospace/artifacts/index.yaml +++ b/infospace/artifacts/index.yaml @@ -224,3 +224,252 @@ artifacts: target: model/task - type: imports target: standard/tagging + - id: profile/small-saas + path: profiles/small-saas/profile.yaml + kind: profile + title: Small SaaS System Profile + provenance: + source_path: infospace/profiles/small-saas/profile.yaml + profile: small-saas + placement_workplan: ITC-WP-0004 + relationships: + - type: conforms_to + target: kernel/itc-core + - type: requires + target: model/landscape + - type: requires + target: model/organization + - type: requires + target: model/governance + - type: requires + target: model/access-control + - type: requires + target: model/security + - type: requires + target: model/data + - type: requires + target: model/devsecops + - type: requires + target: model/network + - type: requires + target: model/observability + - type: requires + target: model/task + - type: requires + target: standard/tagging + - type: requires + target: standard/caring + - id: small-saas/service/billing-portal + path: profiles/small-saas/artifacts/service.billing-portal.yaml + kind: profile-artifact + title: Billing Portal Service + provenance: + profile: small-saas + placement_workplan: ITC-WP-0004 + relationships: + - type: instantiates + target: profile/small-saas + - type: uses + target: model/landscape + - type: part_of + target: small-saas/system/billing-system + - type: owned_by + target: small-saas/team/platform + - id: small-saas/system/billing-system + path: profiles/small-saas/artifacts/system.billing-system.yaml + kind: profile-artifact + title: Small SaaS Billing System + provenance: + profile: small-saas + placement_workplan: ITC-WP-0004 + relationships: + - type: instantiates + target: profile/small-saas + - type: uses + target: model/landscape + - type: serves + target: small-saas/tenant/acme + - type: serves + target: small-saas/tenant/globex + - id: small-saas/tenant/acme + path: profiles/small-saas/artifacts/tenant.acme.yaml + kind: profile-artifact + title: Acme Tenant + provenance: + profile: small-saas + placement_workplan: ITC-WP-0004 + relationships: + - type: instantiates + target: profile/small-saas + - type: uses + target: model/organization + - type: represented_by + target: small-saas/user/ada-admin + - type: isolated_by + target: small-saas/control/namespace-per-tenant + - id: small-saas/tenant/globex + path: profiles/small-saas/artifacts/tenant.globex.yaml + kind: profile-artifact + title: Globex Tenant + provenance: + profile: small-saas + placement_workplan: ITC-WP-0004 + relationships: + - type: instantiates + target: profile/small-saas + - type: uses + target: model/organization + - type: isolated_by + target: small-saas/control/namespace-per-tenant + - id: small-saas/user/ada-admin + path: profiles/small-saas/artifacts/user.ada-admin.yaml + kind: profile-artifact + title: Ada Admin + provenance: + profile: small-saas + placement_workplan: ITC-WP-0004 + relationships: + - type: instantiates + target: profile/small-saas + - type: uses + target: model/organization + - type: uses + target: model/access-control + - type: member_of + target: small-saas/team/platform + - type: has_access_under + target: small-saas/policy/tenant-isolation + - type: access_evidenced_by + target: small-saas/evidence/access-review-2026-05 + - id: small-saas/team/platform + path: profiles/small-saas/artifacts/team.platform.yaml + kind: profile-artifact + title: Platform Team + provenance: + profile: small-saas + placement_workplan: ITC-WP-0004 + relationships: + - type: instantiates + target: profile/small-saas + - type: uses + target: model/organization + - id: small-saas/dataset/subscription-ledger + path: profiles/small-saas/artifacts/dataset.subscription-ledger.yaml + kind: profile-artifact + title: Subscription Ledger Dataset + provenance: + profile: small-saas + placement_workplan: ITC-WP-0004 + relationships: + - type: instantiates + target: profile/small-saas + - type: uses + target: model/data + - type: owned_by + target: small-saas/service/billing-portal + - type: partitioned_for + target: small-saas/tenant/acme + - type: partitioned_for + target: small-saas/tenant/globex + - type: governed_by + target: small-saas/policy/tenant-isolation + - id: small-saas/deployment/production + path: profiles/small-saas/artifacts/deployment.production.yaml + kind: profile-artifact + title: Production Deployment + provenance: + profile: small-saas + placement_workplan: ITC-WP-0004 + relationships: + - type: instantiates + target: profile/small-saas + - type: uses + target: model/devsecops + - type: uses + target: model/network + - type: deploys + target: small-saas/service/billing-portal + - type: separates + target: small-saas/tenant/acme + - type: separates + target: small-saas/tenant/globex + - type: implements + target: small-saas/control/namespace-per-tenant + - id: small-saas/task/onboard-tenant + path: profiles/small-saas/artifacts/task.onboard-tenant.yaml + kind: profile-artifact + title: Onboard Tenant + provenance: + profile: small-saas + placement_workplan: ITC-WP-0004 + relationships: + - type: instantiates + target: profile/small-saas + - type: uses + target: model/task + - type: owned_by + target: small-saas/team/platform + - type: changes + target: small-saas/tenant/acme + - type: governed_by + target: small-saas/policy/tenant-isolation + - id: small-saas/policy/tenant-isolation + path: profiles/small-saas/artifacts/policy.tenant-isolation.yaml + kind: profile-artifact + title: Tenant Isolation Policy + provenance: + profile: small-saas + placement_workplan: ITC-WP-0004 + relationships: + - type: instantiates + target: profile/small-saas + - type: uses + target: model/governance + - type: requires + target: small-saas/control/namespace-per-tenant + - type: evidenced_by + target: small-saas/evidence/access-review-2026-05 + - id: small-saas/control/namespace-per-tenant + path: profiles/small-saas/artifacts/control.namespace-per-tenant.yaml + kind: profile-artifact + title: Namespace Per Tenant Control + provenance: + profile: small-saas + placement_workplan: ITC-WP-0004 + relationships: + - type: instantiates + target: profile/small-saas + - type: uses + target: model/security + - type: uses + target: standard/caring + - type: evidenced_by + target: small-saas/evidence/access-review-2026-05 + - id: small-saas/evidence/access-review-2026-05 + path: profiles/small-saas/artifacts/evidence.access-review-2026-05.yaml + kind: profile-artifact + title: Access Review 2026-05 + provenance: + profile: small-saas + placement_workplan: ITC-WP-0004 + relationships: + - type: instantiates + target: profile/small-saas + - type: uses + target: model/observability + - id: small-saas/incident/cross-tenant-access-attempt + path: profiles/small-saas/artifacts/incident.cross-tenant-access-attempt.yaml + kind: profile-artifact + title: Cross-Tenant Access Attempt + provenance: + profile: small-saas + placement_workplan: ITC-WP-0004 + relationships: + - type: instantiates + target: profile/small-saas + - type: uses + target: model/security + - type: constrained_by + target: small-saas/control/namespace-per-tenant + - type: evidenced_by + target: small-saas/evidence/access-review-2026-05 diff --git a/infospace/examples/small-saas/README.md b/infospace/examples/small-saas/README.md new file mode 100644 index 0000000..206a772 --- /dev/null +++ b/infospace/examples/small-saas/README.md @@ -0,0 +1,14 @@ +# Small SaaS Example + +This example is the first executable profile proof for InfoTechCanon. It +models a compact tenant-aware SaaS service with ownership, tenants, users, +access grants, data partitioning, deployment, governance policy, evidence, and +incident handling. + +Useful commands: + +```bash +PYTHONPATH=src python3 -m info_tech_canon profile inspect small-saas +PYTHONPATH=src python3 -m info_tech_canon profile validate small-saas +PYTHONPATH=src python3 -m info_tech_canon profile graph small-saas +``` diff --git a/infospace/examples/small-saas/demo-commands.yaml b/infospace/examples/small-saas/demo-commands.yaml new file mode 100644 index 0000000..ea46e70 --- /dev/null +++ b/infospace/examples/small-saas/demo-commands.yaml @@ -0,0 +1,29 @@ +profile: small-saas +commands: + inspect: + argv: + - PYTHONPATH=src + - python3 + - -m + - info_tech_canon + - profile + - inspect + - small-saas + validate: + argv: + - PYTHONPATH=src + - python3 + - -m + - info_tech_canon + - profile + - validate + - small-saas + graph: + argv: + - PYTHONPATH=src + - python3 + - -m + - info_tech_canon + - profile + - graph + - small-saas diff --git a/infospace/indexes/artifact-tree.yaml b/infospace/indexes/artifact-tree.yaml index 7b73015..3b0c534 100644 --- a/infospace/indexes/artifact-tree.yaml +++ b/infospace/indexes/artifact-tree.yaml @@ -1,5 +1,5 @@ root: infospace -file_count: 50 +file_count: 67 files: - path: README.md directory: . @@ -19,6 +19,12 @@ files: - path: examples/README.md directory: examples name: README.md +- path: examples/small-saas/README.md + directory: examples/small-saas + name: README.md +- path: examples/small-saas/demo-commands.yaml + directory: examples/small-saas + name: demo-commands.yaml - path: indexes/README.md directory: indexes name: README.md @@ -82,9 +88,54 @@ files: - path: profiles/README.md directory: profiles name: README.md +- path: profiles/small-saas/artifacts/control.namespace-per-tenant.yaml + directory: profiles/small-saas/artifacts + name: control.namespace-per-tenant.yaml +- path: profiles/small-saas/artifacts/dataset.subscription-ledger.yaml + directory: profiles/small-saas/artifacts + name: dataset.subscription-ledger.yaml +- path: profiles/small-saas/artifacts/deployment.production.yaml + directory: profiles/small-saas/artifacts + name: deployment.production.yaml +- path: profiles/small-saas/artifacts/evidence.access-review-2026-05.yaml + directory: profiles/small-saas/artifacts + name: evidence.access-review-2026-05.yaml +- path: profiles/small-saas/artifacts/incident.cross-tenant-access-attempt.yaml + directory: profiles/small-saas/artifacts + name: incident.cross-tenant-access-attempt.yaml +- path: profiles/small-saas/artifacts/policy.tenant-isolation.yaml + directory: profiles/small-saas/artifacts + name: policy.tenant-isolation.yaml +- path: profiles/small-saas/artifacts/service.billing-portal.yaml + directory: profiles/small-saas/artifacts + name: service.billing-portal.yaml +- path: profiles/small-saas/artifacts/system.billing-system.yaml + directory: profiles/small-saas/artifacts + name: system.billing-system.yaml +- path: profiles/small-saas/artifacts/task.onboard-tenant.yaml + directory: profiles/small-saas/artifacts + name: task.onboard-tenant.yaml +- path: profiles/small-saas/artifacts/team.platform.yaml + directory: profiles/small-saas/artifacts + name: team.platform.yaml +- path: profiles/small-saas/artifacts/tenant.acme.yaml + directory: profiles/small-saas/artifacts + name: tenant.acme.yaml +- path: profiles/small-saas/artifacts/tenant.globex.yaml + directory: profiles/small-saas/artifacts + name: tenant.globex.yaml +- path: profiles/small-saas/artifacts/user.ada-admin.yaml + directory: profiles/small-saas/artifacts + name: user.ada-admin.yaml +- path: profiles/small-saas/profile.yaml + directory: profiles/small-saas + name: profile.yaml - path: reports/scaffold-placement.md directory: reports name: scaffold-placement.md +- path: reports/small-saas-profile-proof.md + directory: reports + name: small-saas-profile-proof.md - path: schemas/README.md directory: schemas name: README.md diff --git a/infospace/indexes/concept-ownership.yaml b/infospace/indexes/concept-ownership.yaml index bea5624..e12dfcd 100644 --- a/infospace/indexes/concept-ownership.yaml +++ b/infospace/indexes/concept-ownership.yaml @@ -1,4 +1,4 @@ -concept_count: 30 +concept_count: 44 concepts: - concept: InfoTechCanon Core owner: kernel/itc-core @@ -52,6 +52,62 @@ concepts: owner: model/task path: models/task/InfoTechCanonTaskModel.md source: artifact_title +- concept: Small SaaS System Profile + owner: profile/small-saas + path: profiles/small-saas/profile.yaml + source: artifact_title +- concept: Namespace Per Tenant Control + owner: small-saas/control/namespace-per-tenant + path: profiles/small-saas/artifacts/control.namespace-per-tenant.yaml + source: artifact_title +- concept: Subscription Ledger Dataset + owner: small-saas/dataset/subscription-ledger + path: profiles/small-saas/artifacts/dataset.subscription-ledger.yaml + source: artifact_title +- concept: Production Deployment + owner: small-saas/deployment/production + path: profiles/small-saas/artifacts/deployment.production.yaml + source: artifact_title +- concept: Access Review 2026-05 + owner: small-saas/evidence/access-review-2026-05 + path: profiles/small-saas/artifacts/evidence.access-review-2026-05.yaml + source: artifact_title +- concept: Cross-Tenant Access Attempt + owner: small-saas/incident/cross-tenant-access-attempt + path: profiles/small-saas/artifacts/incident.cross-tenant-access-attempt.yaml + source: artifact_title +- concept: Tenant Isolation Policy + owner: small-saas/policy/tenant-isolation + path: profiles/small-saas/artifacts/policy.tenant-isolation.yaml + source: artifact_title +- concept: Billing Portal Service + owner: small-saas/service/billing-portal + path: profiles/small-saas/artifacts/service.billing-portal.yaml + source: artifact_title +- concept: Small SaaS Billing System + owner: small-saas/system/billing-system + path: profiles/small-saas/artifacts/system.billing-system.yaml + source: artifact_title +- concept: Onboard Tenant + owner: small-saas/task/onboard-tenant + path: profiles/small-saas/artifacts/task.onboard-tenant.yaml + source: artifact_title +- concept: Platform Team + owner: small-saas/team/platform + path: profiles/small-saas/artifacts/team.platform.yaml + source: artifact_title +- concept: Acme Tenant + owner: small-saas/tenant/acme + path: profiles/small-saas/artifacts/tenant.acme.yaml + source: artifact_title +- concept: Globex Tenant + owner: small-saas/tenant/globex + path: profiles/small-saas/artifacts/tenant.globex.yaml + source: artifact_title +- concept: Ada Admin + owner: small-saas/user/ada-admin + path: profiles/small-saas/artifacts/user.ada-admin.yaml + source: artifact_title - concept: InfoTechCanon CARING Access Governance Standard owner: standard/caring path: standards/caring/InfoTechCanonCaringAccessGovernanceStandard.md diff --git a/infospace/indexes/import-matrix.yaml b/infospace/indexes/import-matrix.yaml index 5f505b1..676a65a 100644 --- a/infospace/indexes/import-matrix.yaml +++ b/infospace/indexes/import-matrix.yaml @@ -12,6 +12,20 @@ artifacts: - model/organization - model/security - model/task +- profile/small-saas +- small-saas/control/namespace-per-tenant +- small-saas/dataset/subscription-ledger +- small-saas/deployment/production +- small-saas/evidence/access-review-2026-05 +- small-saas/incident/cross-tenant-access-attempt +- small-saas/policy/tenant-isolation +- small-saas/service/billing-portal +- small-saas/system/billing-system +- small-saas/task/onboard-tenant +- small-saas/team/platform +- small-saas/tenant/acme +- small-saas/tenant/globex +- small-saas/user/ada-admin - standard/caring - standard/tagging rows: @@ -105,6 +119,170 @@ rows: targets: kernel/itc-core: - conforms_to +- artifact: profile/small-saas + targets: + kernel/itc-core: + - conforms_to + model/access-control: + - requires + model/data: + - requires + model/devsecops: + - requires + model/governance: + - requires + model/landscape: + - requires + model/network: + - requires + model/observability: + - requires + model/organization: + - requires + model/security: + - requires + model/task: + - requires + standard/caring: + - requires + standard/tagging: + - requires +- artifact: small-saas/control/namespace-per-tenant + targets: + model/security: + - uses + profile/small-saas: + - instantiates + small-saas/evidence/access-review-2026-05: + - evidenced_by + standard/caring: + - uses +- artifact: small-saas/dataset/subscription-ledger + targets: + model/data: + - uses + profile/small-saas: + - instantiates + small-saas/policy/tenant-isolation: + - governed_by + small-saas/service/billing-portal: + - owned_by + small-saas/tenant/acme: + - partitioned_for + small-saas/tenant/globex: + - partitioned_for +- artifact: small-saas/deployment/production + targets: + model/devsecops: + - uses + model/network: + - uses + profile/small-saas: + - instantiates + small-saas/control/namespace-per-tenant: + - implements + small-saas/service/billing-portal: + - deploys + small-saas/tenant/acme: + - separates + small-saas/tenant/globex: + - separates +- artifact: small-saas/evidence/access-review-2026-05 + targets: + model/observability: + - uses + profile/small-saas: + - instantiates +- artifact: small-saas/incident/cross-tenant-access-attempt + targets: + model/security: + - uses + profile/small-saas: + - instantiates + small-saas/control/namespace-per-tenant: + - constrained_by + small-saas/evidence/access-review-2026-05: + - evidenced_by +- artifact: small-saas/policy/tenant-isolation + targets: + model/governance: + - uses + profile/small-saas: + - instantiates + small-saas/control/namespace-per-tenant: + - requires + small-saas/evidence/access-review-2026-05: + - evidenced_by +- artifact: small-saas/service/billing-portal + targets: + model/landscape: + - uses + profile/small-saas: + - instantiates + small-saas/system/billing-system: + - part_of + small-saas/team/platform: + - owned_by +- artifact: small-saas/system/billing-system + targets: + model/landscape: + - uses + profile/small-saas: + - instantiates + small-saas/tenant/acme: + - serves + small-saas/tenant/globex: + - serves +- artifact: small-saas/task/onboard-tenant + targets: + model/task: + - uses + profile/small-saas: + - instantiates + small-saas/policy/tenant-isolation: + - governed_by + small-saas/team/platform: + - owned_by + small-saas/tenant/acme: + - changes +- artifact: small-saas/team/platform + targets: + model/organization: + - uses + profile/small-saas: + - instantiates +- artifact: small-saas/tenant/acme + targets: + model/organization: + - uses + profile/small-saas: + - instantiates + small-saas/control/namespace-per-tenant: + - isolated_by + small-saas/user/ada-admin: + - represented_by +- artifact: small-saas/tenant/globex + targets: + model/organization: + - uses + profile/small-saas: + - instantiates + small-saas/control/namespace-per-tenant: + - isolated_by +- artifact: small-saas/user/ada-admin + targets: + model/access-control: + - uses + model/organization: + - uses + profile/small-saas: + - instantiates + small-saas/evidence/access-review-2026-05: + - access_evidenced_by + small-saas/policy/tenant-isolation: + - has_access_under + small-saas/team/platform: + - member_of - artifact: standard/caring targets: kernel/itc-core: diff --git a/infospace/profiles/small-saas/artifacts/control.namespace-per-tenant.yaml b/infospace/profiles/small-saas/artifacts/control.namespace-per-tenant.yaml new file mode 100644 index 0000000..c259963 --- /dev/null +++ b/infospace/profiles/small-saas/artifacts/control.namespace-per-tenant.yaml @@ -0,0 +1,14 @@ +id: small-saas/control/namespace-per-tenant +kind: control +title: Namespace Per Tenant Control +profile: small-saas +policy_id: small-saas/policy/tenant-isolation +claim: Every production tenant has a distinct runtime namespace and data partition. +control_type: preventive +evidence_ids: + - small-saas/evidence/access-review-2026-05 +relationships: + - type: satisfies + target: small-saas/policy/tenant-isolation + - type: evidenced_by + target: small-saas/evidence/access-review-2026-05 diff --git a/infospace/profiles/small-saas/artifacts/dataset.subscription-ledger.yaml b/infospace/profiles/small-saas/artifacts/dataset.subscription-ledger.yaml new file mode 100644 index 0000000..4ffd9c4 --- /dev/null +++ b/infospace/profiles/small-saas/artifacts/dataset.subscription-ledger.yaml @@ -0,0 +1,21 @@ +id: small-saas/dataset/subscription-ledger +kind: dataset +title: Subscription Ledger Dataset +profile: small-saas +owner_service: small-saas/service/billing-portal +classification: customer-confidential +tenant_scope: per-tenant +tenant_ids: + - small-saas/tenant/acme + - small-saas/tenant/globex +evidence_ids: + - small-saas/evidence/access-review-2026-05 +relationships: + - type: owned_by + target: small-saas/service/billing-portal + - type: partitioned_for + target: small-saas/tenant/acme + - type: partitioned_for + target: small-saas/tenant/globex + - type: governed_by + target: small-saas/policy/tenant-isolation diff --git a/infospace/profiles/small-saas/artifacts/deployment.production.yaml b/infospace/profiles/small-saas/artifacts/deployment.production.yaml new file mode 100644 index 0000000..6b54e24 --- /dev/null +++ b/infospace/profiles/small-saas/artifacts/deployment.production.yaml @@ -0,0 +1,22 @@ +id: small-saas/deployment/production +kind: deployment +title: Production Deployment +profile: small-saas +service_id: small-saas/service/billing-portal +environment: production +namespace_strategy: namespace-per-tenant +tenant_namespaces: + small-saas/tenant/acme: tenant-acme + small-saas/tenant/globex: tenant-globex +network_exposure: public-ingress-authenticated +evidence_ids: + - small-saas/evidence/access-review-2026-05 +relationships: + - type: deploys + target: small-saas/service/billing-portal + - type: separates + target: small-saas/tenant/acme + - type: separates + target: small-saas/tenant/globex + - type: implements + target: small-saas/control/namespace-per-tenant diff --git a/infospace/profiles/small-saas/artifacts/evidence.access-review-2026-05.yaml b/infospace/profiles/small-saas/artifacts/evidence.access-review-2026-05.yaml new file mode 100644 index 0000000..2f3772e --- /dev/null +++ b/infospace/profiles/small-saas/artifacts/evidence.access-review-2026-05.yaml @@ -0,0 +1,18 @@ +id: small-saas/evidence/access-review-2026-05 +kind: evidence +title: Access Review 2026-05 +profile: small-saas +evidence_type: review-record +date: "2026-05-23" +supports: + - small-saas/service/billing-portal + - small-saas/policy/tenant-isolation + - small-saas/control/namespace-per-tenant + - small-saas/incident/cross-tenant-access-attempt +relationships: + - type: supports + target: small-saas/policy/tenant-isolation + - type: supports + target: small-saas/control/namespace-per-tenant + - type: supports + target: small-saas/incident/cross-tenant-access-attempt diff --git a/infospace/profiles/small-saas/artifacts/incident.cross-tenant-access-attempt.yaml b/infospace/profiles/small-saas/artifacts/incident.cross-tenant-access-attempt.yaml new file mode 100644 index 0000000..46a8fc0 --- /dev/null +++ b/infospace/profiles/small-saas/artifacts/incident.cross-tenant-access-attempt.yaml @@ -0,0 +1,15 @@ +id: small-saas/incident/cross-tenant-access-attempt +kind: incident +title: Cross-Tenant Access Attempt +profile: small-saas +status: resolved +tenant_id: small-saas/tenant/acme +control_ids: + - small-saas/control/namespace-per-tenant +evidence_ids: + - small-saas/evidence/access-review-2026-05 +relationships: + - type: constrained_by + target: small-saas/control/namespace-per-tenant + - type: evidenced_by + target: small-saas/evidence/access-review-2026-05 diff --git a/infospace/profiles/small-saas/artifacts/policy.tenant-isolation.yaml b/infospace/profiles/small-saas/artifacts/policy.tenant-isolation.yaml new file mode 100644 index 0000000..c867279 --- /dev/null +++ b/infospace/profiles/small-saas/artifacts/policy.tenant-isolation.yaml @@ -0,0 +1,15 @@ +id: small-saas/policy/tenant-isolation +kind: policy +title: Tenant Isolation Policy +profile: small-saas +scope: service-and-data-plane +statement: Tenant requests, namespaces, access grants, and data partitions must not cross tenant boundaries. +control_ids: + - small-saas/control/namespace-per-tenant +evidence_ids: + - small-saas/evidence/access-review-2026-05 +relationships: + - type: requires + target: small-saas/control/namespace-per-tenant + - type: evidenced_by + target: small-saas/evidence/access-review-2026-05 diff --git a/infospace/profiles/small-saas/artifacts/service.billing-portal.yaml b/infospace/profiles/small-saas/artifacts/service.billing-portal.yaml new file mode 100644 index 0000000..ef95690 --- /dev/null +++ b/infospace/profiles/small-saas/artifacts/service.billing-portal.yaml @@ -0,0 +1,28 @@ +id: small-saas/service/billing-portal +kind: service +title: Billing Portal Service +profile: small-saas +system_id: small-saas/system/billing-system +owner_team: small-saas/team/platform +runtime_deployment: small-saas/deployment/production +datasets: + - small-saas/dataset/subscription-ledger +controls: + - small-saas/control/namespace-per-tenant +policies: + - small-saas/policy/tenant-isolation +evidence_ids: + - small-saas/evidence/access-review-2026-05 +relationships: + - type: part_of + target: small-saas/system/billing-system + - type: owned_by + target: small-saas/team/platform + - type: stores + target: small-saas/dataset/subscription-ledger + - type: deployed_as + target: small-saas/deployment/production + - type: governed_by + target: small-saas/policy/tenant-isolation + - type: protected_by + target: small-saas/control/namespace-per-tenant diff --git a/infospace/profiles/small-saas/artifacts/system.billing-system.yaml b/infospace/profiles/small-saas/artifacts/system.billing-system.yaml new file mode 100644 index 0000000..d07683f --- /dev/null +++ b/infospace/profiles/small-saas/artifacts/system.billing-system.yaml @@ -0,0 +1,17 @@ +id: small-saas/system/billing-system +kind: system +title: Small SaaS Billing System +profile: small-saas +service_ids: + - small-saas/service/billing-portal +tenant_ids: + - small-saas/tenant/acme + - small-saas/tenant/globex +owner_team: small-saas/team/platform +relationships: + - type: includes + target: small-saas/service/billing-portal + - type: serves + target: small-saas/tenant/acme + - type: serves + target: small-saas/tenant/globex diff --git a/infospace/profiles/small-saas/artifacts/task.onboard-tenant.yaml b/infospace/profiles/small-saas/artifacts/task.onboard-tenant.yaml new file mode 100644 index 0000000..9b075ca --- /dev/null +++ b/infospace/profiles/small-saas/artifacts/task.onboard-tenant.yaml @@ -0,0 +1,16 @@ +id: small-saas/task/onboard-tenant +kind: task +title: Onboard Tenant +profile: small-saas +owner_team: small-saas/team/platform +status: ready +target_tenant: small-saas/tenant/acme +evidence_ids: + - small-saas/evidence/access-review-2026-05 +relationships: + - type: owned_by + target: small-saas/team/platform + - type: changes + target: small-saas/tenant/acme + - type: governed_by + target: small-saas/policy/tenant-isolation diff --git a/infospace/profiles/small-saas/artifacts/team.platform.yaml b/infospace/profiles/small-saas/artifacts/team.platform.yaml new file mode 100644 index 0000000..86e26db --- /dev/null +++ b/infospace/profiles/small-saas/artifacts/team.platform.yaml @@ -0,0 +1,14 @@ +id: small-saas/team/platform +kind: team +title: Platform Team +profile: small-saas +owner_user: small-saas/user/ada-admin +responsibilities: + - service ownership + - tenant onboarding + - access review +relationships: + - type: owns + target: small-saas/service/billing-portal + - type: performs + target: small-saas/task/onboard-tenant diff --git a/infospace/profiles/small-saas/artifacts/tenant.acme.yaml b/infospace/profiles/small-saas/artifacts/tenant.acme.yaml new file mode 100644 index 0000000..634dffc --- /dev/null +++ b/infospace/profiles/small-saas/artifacts/tenant.acme.yaml @@ -0,0 +1,12 @@ +id: small-saas/tenant/acme +kind: tenant +title: Acme Tenant +profile: small-saas +namespace: tenant-acme +data_partition: subscription-ledger:tenant=acme +primary_user: small-saas/user/ada-admin +relationships: + - type: represented_by + target: small-saas/user/ada-admin + - type: isolated_by + target: small-saas/control/namespace-per-tenant diff --git a/infospace/profiles/small-saas/artifacts/tenant.globex.yaml b/infospace/profiles/small-saas/artifacts/tenant.globex.yaml new file mode 100644 index 0000000..73faea2 --- /dev/null +++ b/infospace/profiles/small-saas/artifacts/tenant.globex.yaml @@ -0,0 +1,9 @@ +id: small-saas/tenant/globex +kind: tenant +title: Globex Tenant +profile: small-saas +namespace: tenant-globex +data_partition: subscription-ledger:tenant=globex +relationships: + - type: isolated_by + target: small-saas/control/namespace-per-tenant diff --git a/infospace/profiles/small-saas/artifacts/user.ada-admin.yaml b/infospace/profiles/small-saas/artifacts/user.ada-admin.yaml new file mode 100644 index 0000000..867d69f --- /dev/null +++ b/infospace/profiles/small-saas/artifacts/user.ada-admin.yaml @@ -0,0 +1,19 @@ +id: small-saas/user/ada-admin +kind: user +title: Ada Admin +profile: small-saas +person_type: human +teams: + - small-saas/team/platform +access_grants: + - role: tenant-admin + tenant_id: small-saas/tenant/acme + policy_id: small-saas/policy/tenant-isolation + evidence_id: small-saas/evidence/access-review-2026-05 +relationships: + - type: member_of + target: small-saas/team/platform + - type: has_access_under + target: small-saas/policy/tenant-isolation + - type: access_evidenced_by + target: small-saas/evidence/access-review-2026-05 diff --git a/infospace/profiles/small-saas/profile.yaml b/infospace/profiles/small-saas/profile.yaml new file mode 100644 index 0000000..3e053d8 --- /dev/null +++ b/infospace/profiles/small-saas/profile.yaml @@ -0,0 +1,112 @@ +id: small-saas +kind: profile +profile: small-saas +title: Small SaaS System Profile +scope: A compact tenant-aware SaaS service with users, teams, data, access, deployment, governance evidence, and incident handling. +status: proof +conformance_level: profile-proof +assumptions: + - The SaaS product has a single service boundary and two example tenants. + - Tenants are separated by namespace and data partitioning claims. + - User management is represented through users, teams, access grants, policies, controls, and evidence. + - Runtime concerns are represented by one production deployment. +required_standards: + - kernel/itc-core + - model/landscape + - model/organization + - model/governance + - model/task + - model/access-control + - model/security + - model/data + - model/devsecops + - model/network + - model/observability + - standard/tagging + - standard/caring +required_concepts: + service: + status: required + model: model/landscape + system: + status: required + model: model/landscape + tenant: + status: required + model: model/organization + user: + status: required + model: model/organization + team: + status: required + model: model/organization + dataset: + status: required + model: model/data + deployment: + status: required + model: model/devsecops + task: + status: required + model: model/task + policy: + status: required + model: model/governance + control: + status: required + model: model/security + evidence: + status: required + model: model/observability + incident: + status: required + model: model/security +optional_concepts: + billing-plan: + status: optional + model: model/data + notification: + status: optional + model: model/observability +out_of_scope: + - multi-region disaster recovery + - tenant-managed encryption keys + - marketplace billing integrations +artifact_ids: + - profile/small-saas + - small-saas/service/billing-portal + - small-saas/system/billing-system + - small-saas/tenant/acme + - small-saas/tenant/globex + - small-saas/user/ada-admin + - small-saas/team/platform + - small-saas/dataset/subscription-ledger + - small-saas/deployment/production + - small-saas/task/onboard-tenant + - small-saas/policy/tenant-isolation + - small-saas/control/namespace-per-tenant + - small-saas/evidence/access-review-2026-05 + - small-saas/incident/cross-tenant-access-attempt +validation_rules: + required_artifact_kinds: + - service + - system + - tenant + - user + - team + - dataset + - deployment + - task + - policy + - control + - evidence + - incident + service_ownership: required + tenant_namespace_separation: required + user_management_trace: required + access_control_trace: required + governance_evidence: required +demo_commands: + - PYTHONPATH=src python3 -m info_tech_canon profile inspect small-saas + - PYTHONPATH=src python3 -m info_tech_canon profile validate small-saas + - PYTHONPATH=src python3 -m info_tech_canon profile graph small-saas diff --git a/infospace/reports/small-saas-profile-proof.md b/infospace/reports/small-saas-profile-proof.md new file mode 100644 index 0000000..3a1ac0d --- /dev/null +++ b/infospace/reports/small-saas-profile-proof.md @@ -0,0 +1,44 @@ +# Small SaaS Profile Proof + +**Workplan:** ITC-WP-0004 +**Profile:** `small-saas` +**Status:** executable proof + +## Purpose + +The `small-saas` profile demonstrates that the canon can guide a practical +service shape before the larger CARING benchmark exists. It connects service +ownership, users, tenants, access, data, runtime, deployment, governance, +observability, and task tracking. + +## Validation Surface + +```bash +PYTHONPATH=src python3 -m info_tech_canon profile inspect small-saas +PYTHONPATH=src python3 -m info_tech_canon profile validate small-saas +PYTHONPATH=src python3 -m info_tech_canon profile graph small-saas +``` + +## Graph Slice + +The profile graph includes the profile artifact, the small SaaS example +artifacts, and their direct canon anchors. + +```mermaid +graph TD + profile_small_saas["profile/small-saas"] -->|conforms_to| core["kernel/itc-core"] + service["small-saas/service/billing-portal"] -->|part_of| system["small-saas/system/billing-system"] + service -->|owned_by| team["small-saas/team/platform"] + service -->|governed_by| policy["small-saas/policy/tenant-isolation"] + service -->|protected_by| control["small-saas/control/namespace-per-tenant"] + deployment["small-saas/deployment/production"] -->|separates| acme["small-saas/tenant/acme"] + deployment -->|separates| globex["small-saas/tenant/globex"] + user["small-saas/user/ada-admin"] -->|has_access_under| policy + policy -->|evidenced_by| evidence["small-saas/evidence/access-review-2026-05"] + incident["small-saas/incident/cross-tenant-access-attempt"] -->|constrained_by| control +``` + +## Result + +The proof is suitable as a baseline for the upcoming `user-engine` evaluation +pack and for `railiance-fabric` entity/edge capture comparisons. diff --git a/infospace/validation/latest.json b/infospace/validation/latest.json index eb4ece7..9ae9fde 100644 --- a/infospace/validation/latest.json +++ b/infospace/validation/latest.json @@ -1,14 +1,14 @@ { "details": { - "artifact_count": 15, - "relationship_count": 45 + "artifact_count": 29, + "relationship_count": 113 }, "errors": [], "metrics": { "coherence_components": 1.0, "consistency_cycles": 0.0, "coverage_ratio": 1.0, - "granularity_entropy": 1.103307408607834, + "granularity_entropy": 1.7490339557881076, "redundancy_ratio": 0.0 }, "ok": true, @@ -17,10 +17,6 @@ "code": "missing_optional_concepts_dir", "path": "infospace/concepts" }, - { - "code": "empty_optional_collection", - "path": "infospace/profiles" - }, { "code": "empty_optional_collection", "path": "infospace/patterns" @@ -32,10 +28,6 @@ { "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 index daa73e6..d921957 100644 --- a/infospace/views/by-concept.md +++ b/infospace/views/by-concept.md @@ -2,7 +2,7 @@ # By Concept -Concept count: **30** +Concept count: **44** | Concept | Owner | Source | | --- | --- | --- | @@ -19,6 +19,20 @@ Concept count: **30** | InfoTechCanon Organization Model | `model/organization` | `artifact_title` | | InfoTechCanon Security Model | `model/security` | `artifact_title` | | InfoTechCanon Task Model | `model/task` | `artifact_title` | +| Small SaaS System Profile | `profile/small-saas` | `artifact_title` | +| Namespace Per Tenant Control | `small-saas/control/namespace-per-tenant` | `artifact_title` | +| Subscription Ledger Dataset | `small-saas/dataset/subscription-ledger` | `artifact_title` | +| Production Deployment | `small-saas/deployment/production` | `artifact_title` | +| Access Review 2026-05 | `small-saas/evidence/access-review-2026-05` | `artifact_title` | +| Cross-Tenant Access Attempt | `small-saas/incident/cross-tenant-access-attempt` | `artifact_title` | +| Tenant Isolation Policy | `small-saas/policy/tenant-isolation` | `artifact_title` | +| Billing Portal Service | `small-saas/service/billing-portal` | `artifact_title` | +| Small SaaS Billing System | `small-saas/system/billing-system` | `artifact_title` | +| Onboard Tenant | `small-saas/task/onboard-tenant` | `artifact_title` | +| Platform Team | `small-saas/team/platform` | `artifact_title` | +| Acme Tenant | `small-saas/tenant/acme` | `artifact_title` | +| Globex Tenant | `small-saas/tenant/globex` | `artifact_title` | +| Ada Admin | `small-saas/user/ada-admin` | `artifact_title` | | InfoTechCanon CARING Access Governance Standard | `standard/caring` | `artifact_title` | | CARINGAccessDescriptor | `standard/caring` | `frontmatter.owned_concepts` | | CARINGCanonicalRole | `standard/caring` | `frontmatter.owned_concepts` | diff --git a/infospace/views/by-mapping-target.md b/infospace/views/by-mapping-target.md index 5c4f9a6..d4a75a0 100644 --- a/infospace/views/by-mapping-target.md +++ b/infospace/views/by-mapping-target.md @@ -16,6 +16,7 @@ - `model/organization` via `conforms_to` - `model/security` via `conforms_to` - `model/task` via `conforms_to` +- `profile/small-saas` via `conforms_to` - `standard/caring` via `conforms_to` - `standard/tagging` via `conforms_to` @@ -23,16 +24,22 @@ - `kernel/itc-kernel-map` via `maps` - `model/security` via `uses` +- `profile/small-saas` via `requires` +- `small-saas/user/ada-admin` via `uses` - `standard/caring` via `imports` ## `model/data` - `kernel/itc-kernel-map` via `maps` +- `profile/small-saas` via `requires` +- `small-saas/dataset/subscription-ledger` via `uses` - `standard/caring` via `imports` ## `model/devsecops` - `kernel/itc-kernel-map` via `maps` +- `profile/small-saas` via `requires` +- `small-saas/deployment/production` via `uses` - `standard/caring` via `imports` ## `model/governance` @@ -40,6 +47,8 @@ - `kernel/itc-kernel-map` via `maps` - `model/access-control` via `uses` - `model/data` via `uses` +- `profile/small-saas` via `requires` +- `small-saas/policy/tenant-isolation` via `uses` - `standard/caring` via `imports` ## `model/information-space` @@ -49,21 +58,33 @@ ## `model/landscape` - `kernel/itc-kernel-map` via `maps` +- `profile/small-saas` via `requires` +- `small-saas/service/billing-portal` via `uses` +- `small-saas/system/billing-system` via `uses` ## `model/network` - `kernel/itc-kernel-map` via `maps` +- `profile/small-saas` via `requires` +- `small-saas/deployment/production` via `uses` - `standard/caring` via `imports` ## `model/observability` - `kernel/itc-kernel-map` via `maps` +- `profile/small-saas` via `requires` +- `small-saas/evidence/access-review-2026-05` via `uses` - `standard/caring` via `imports` ## `model/organization` - `kernel/itc-kernel-map` via `maps` - `model/access-control` via `uses` +- `profile/small-saas` via `requires` +- `small-saas/team/platform` via `uses` +- `small-saas/tenant/acme` via `uses` +- `small-saas/tenant/globex` via `uses` +- `small-saas/user/ada-admin` via `uses` - `standard/caring` via `imports` ## `model/security` @@ -71,20 +92,97 @@ - `kernel/itc-kernel-map` via `maps` - `model/devsecops` via `uses` - `model/network` via `uses` +- `profile/small-saas` via `requires` +- `small-saas/control/namespace-per-tenant` via `uses` +- `small-saas/incident/cross-tenant-access-attempt` via `uses` - `standard/caring` via `imports` ## `model/task` - `kernel/itc-kernel-map` via `maps` - `model/observability` via `uses` +- `profile/small-saas` via `requires` +- `small-saas/task/onboard-tenant` via `uses` - `standard/caring` via `imports` - `standard/tagging` via `imports` +## `profile/small-saas` + +- `small-saas/control/namespace-per-tenant` via `instantiates` +- `small-saas/dataset/subscription-ledger` via `instantiates` +- `small-saas/deployment/production` via `instantiates` +- `small-saas/evidence/access-review-2026-05` via `instantiates` +- `small-saas/incident/cross-tenant-access-attempt` via `instantiates` +- `small-saas/policy/tenant-isolation` via `instantiates` +- `small-saas/service/billing-portal` via `instantiates` +- `small-saas/system/billing-system` via `instantiates` +- `small-saas/task/onboard-tenant` via `instantiates` +- `small-saas/team/platform` via `instantiates` +- `small-saas/tenant/acme` via `instantiates` +- `small-saas/tenant/globex` via `instantiates` +- `small-saas/user/ada-admin` via `instantiates` + +## `small-saas/control/namespace-per-tenant` + +- `small-saas/deployment/production` via `implements` +- `small-saas/incident/cross-tenant-access-attempt` via `constrained_by` +- `small-saas/policy/tenant-isolation` via `requires` +- `small-saas/tenant/acme` via `isolated_by` +- `small-saas/tenant/globex` via `isolated_by` + +## `small-saas/evidence/access-review-2026-05` + +- `small-saas/control/namespace-per-tenant` via `evidenced_by` +- `small-saas/incident/cross-tenant-access-attempt` via `evidenced_by` +- `small-saas/policy/tenant-isolation` via `evidenced_by` +- `small-saas/user/ada-admin` via `access_evidenced_by` + +## `small-saas/policy/tenant-isolation` + +- `small-saas/dataset/subscription-ledger` via `governed_by` +- `small-saas/task/onboard-tenant` via `governed_by` +- `small-saas/user/ada-admin` via `has_access_under` + +## `small-saas/service/billing-portal` + +- `small-saas/dataset/subscription-ledger` via `owned_by` +- `small-saas/deployment/production` via `deploys` + +## `small-saas/system/billing-system` + +- `small-saas/service/billing-portal` via `part_of` + +## `small-saas/team/platform` + +- `small-saas/service/billing-portal` via `owned_by` +- `small-saas/task/onboard-tenant` via `owned_by` +- `small-saas/user/ada-admin` via `member_of` + +## `small-saas/tenant/acme` + +- `small-saas/dataset/subscription-ledger` via `partitioned_for` +- `small-saas/deployment/production` via `separates` +- `small-saas/system/billing-system` via `serves` +- `small-saas/task/onboard-tenant` via `changes` + +## `small-saas/tenant/globex` + +- `small-saas/dataset/subscription-ledger` via `partitioned_for` +- `small-saas/deployment/production` via `separates` +- `small-saas/system/billing-system` via `serves` + +## `small-saas/user/ada-admin` + +- `small-saas/tenant/acme` via `represented_by` + ## `standard/caring` - `kernel/itc-kernel-map` via `maps` +- `profile/small-saas` via `requires` +- `small-saas/control/namespace-per-tenant` via `uses` ## `standard/tagging` - `kernel/itc-kernel-map` via `maps` +- `profile/small-saas` via `requires` - `standard/caring` via `imports` diff --git a/infospace/views/by-profile.md b/infospace/views/by-profile.md index 1de074d..cadb4a0 100644 --- a/infospace/views/by-profile.md +++ b/infospace/views/by-profile.md @@ -2,4 +2,6 @@ # By Profile -No profiles have been registered yet. +## small-saas + +- Path: `profiles/small-saas/profile.yaml` diff --git a/infospace/views/import-matrix.md b/infospace/views/import-matrix.md index f7baa0f..1cd3975 100644 --- a/infospace/views/import-matrix.md +++ b/infospace/views/import-matrix.md @@ -2,20 +2,34 @@ # 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` | | | +| 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` | `profile/small-saas` | `small-saas/control/namespace-per-tenant` | `small-saas/dataset/subscription-ledger` | `small-saas/deployment/production` | `small-saas/evidence/access-review-2026-05` | `small-saas/incident/cross-tenant-access-attempt` | `small-saas/policy/tenant-isolation` | `small-saas/service/billing-portal` | `small-saas/system/billing-system` | `small-saas/task/onboard-tenant` | `small-saas/team/platform` | `small-saas/tenant/acme` | `small-saas/tenant/globex` | `small-saas/user/ada-admin` | `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` | | | | | | | | | | | | | | | | | | | | | | | | | | | | | +| `profile/small-saas` | `conforms_to` | | `requires` | `requires` | `requires` | `requires` | | `requires` | `requires` | `requires` | `requires` | `requires` | `requires` | | | | | | | | | | | | | | | `requires` | `requires` | +| `small-saas/control/namespace-per-tenant` | | | | | | | | | | | | `uses` | | `instantiates` | | | | `evidenced_by` | | | | | | | | | | `uses` | | +| `small-saas/dataset/subscription-ledger` | | | | `uses` | | | | | | | | | | `instantiates` | | | | | | `governed_by` | `owned_by` | | | | `partitioned_for` | `partitioned_for` | | | | +| `small-saas/deployment/production` | | | | | `uses` | | | | `uses` | | | | | `instantiates` | `implements` | | | | | | `deploys` | | | | `separates` | `separates` | | | | +| `small-saas/evidence/access-review-2026-05` | | | | | | | | | | `uses` | | | | `instantiates` | | | | | | | | | | | | | | | | +| `small-saas/incident/cross-tenant-access-attempt` | | | | | | | | | | | | `uses` | | `instantiates` | `constrained_by` | | | `evidenced_by` | | | | | | | | | | | | +| `small-saas/policy/tenant-isolation` | | | | | | `uses` | | | | | | | | `instantiates` | `requires` | | | `evidenced_by` | | | | | | | | | | | | +| `small-saas/service/billing-portal` | | | | | | | | `uses` | | | | | | `instantiates` | | | | | | | | `part_of` | | `owned_by` | | | | | | +| `small-saas/system/billing-system` | | | | | | | | `uses` | | | | | | `instantiates` | | | | | | | | | | | `serves` | `serves` | | | | +| `small-saas/task/onboard-tenant` | | | | | | | | | | | | | `uses` | `instantiates` | | | | | | `governed_by` | | | | `owned_by` | `changes` | | | | | +| `small-saas/team/platform` | | | | | | | | | | | `uses` | | | `instantiates` | | | | | | | | | | | | | | | | +| `small-saas/tenant/acme` | | | | | | | | | | | `uses` | | | `instantiates` | `isolated_by` | | | | | | | | | | | | `represented_by` | | | +| `small-saas/tenant/globex` | | | | | | | | | | | `uses` | | | `instantiates` | `isolated_by` | | | | | | | | | | | | | | | +| `small-saas/user/ada-admin` | | | `uses` | | | | | | | | `uses` | | | `instantiates` | | | | `access_evidenced_by` | | `has_access_under` | | | | `member_of` | | | | | | +| `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 index 017d272..4328141 100644 --- a/infospace/views/kernel-overview.md +++ b/infospace/views/kernel-overview.md @@ -3,17 +3,37 @@ # Kernel Overview - Infospace: `canon` -- Artifacts: 15 +- Artifacts: 29 ## Artifact Kinds - `kernel`: 2 - `model`: 11 +- `profile`: 1 +- `profile-artifact`: 13 - `standard`: 2 ## Relationship Types -- `conforms_to`: 13 +- `access_evidenced_by`: 1 +- `changes`: 1 +- `conforms_to`: 14 +- `constrained_by`: 1 +- `deploys`: 1 +- `evidenced_by`: 3 +- `governed_by`: 2 +- `has_access_under`: 1 +- `implements`: 1 - `imports`: 11 +- `instantiates`: 13 +- `isolated_by`: 2 - `maps`: 14 -- `uses`: 7 +- `member_of`: 1 +- `owned_by`: 3 +- `part_of`: 1 +- `partitioned_for`: 2 +- `represented_by`: 1 +- `requires`: 13 +- `separates`: 2 +- `serves`: 2 +- `uses`: 23 diff --git a/infospace/views/repository-tree.md b/infospace/views/repository-tree.md index b9ba248..d22eb0a 100644 --- a/infospace/views/repository-tree.md +++ b/infospace/views/repository-tree.md @@ -2,7 +2,7 @@ # Repository Tree -File count: **50** +File count: **67** - `README.md` - `agent/README.md` @@ -10,6 +10,8 @@ File count: **50** - `artifacts/index.yaml` - `assimilation/README.md` - `examples/README.md` +- `examples/small-saas/README.md` +- `examples/small-saas/demo-commands.yaml` - `indexes/README.md` - `indexes/artifact-tree.yaml` - `indexes/concept-ownership.yaml` @@ -31,7 +33,22 @@ File count: **50** - `models/task/InfoTechCanonTaskModel.md` - `patterns/README.md` - `profiles/README.md` +- `profiles/small-saas/artifacts/control.namespace-per-tenant.yaml` +- `profiles/small-saas/artifacts/dataset.subscription-ledger.yaml` +- `profiles/small-saas/artifacts/deployment.production.yaml` +- `profiles/small-saas/artifacts/evidence.access-review-2026-05.yaml` +- `profiles/small-saas/artifacts/incident.cross-tenant-access-attempt.yaml` +- `profiles/small-saas/artifacts/policy.tenant-isolation.yaml` +- `profiles/small-saas/artifacts/service.billing-portal.yaml` +- `profiles/small-saas/artifacts/system.billing-system.yaml` +- `profiles/small-saas/artifacts/task.onboard-tenant.yaml` +- `profiles/small-saas/artifacts/team.platform.yaml` +- `profiles/small-saas/artifacts/tenant.acme.yaml` +- `profiles/small-saas/artifacts/tenant.globex.yaml` +- `profiles/small-saas/artifacts/user.ada-admin.yaml` +- `profiles/small-saas/profile.yaml` - `reports/scaffold-placement.md` +- `reports/small-saas-profile-proof.md` - `schemas/README.md` - `schemas/agent-brief.schema.yaml` - `schemas/assimilation.schema.yaml` diff --git a/src/info_tech_canon/__init__.py b/src/info_tech_canon/__init__.py index f2727b7..e82c97d 100644 --- a/src/info_tech_canon/__init__.py +++ b/src/info_tech_canon/__init__.py @@ -11,7 +11,9 @@ from .service import ( list_models, list_standards, list_views, + profile_graph, profile_inspect, + profile_validate, read_view, validate_canon, write_validation_report, @@ -28,7 +30,9 @@ __all__ = [ "list_models", "list_standards", "list_views", + "profile_graph", "profile_inspect", + "profile_validate", "read_view", "validate_canon", "write_validation_report", diff --git a/src/info_tech_canon/api.py b/src/info_tech_canon/api.py index 20f08e4..d8df5b3 100644 --- a/src/info_tech_canon/api.py +++ b/src/info_tech_canon/api.py @@ -15,7 +15,9 @@ from .service import ( list_models, list_standards, list_views, + profile_graph, profile_inspect, + profile_validate, read_view, validate_canon, ) @@ -96,6 +98,14 @@ def _route( if path.startswith("/profiles/") and path.endswith("/inspect"): profile = path.removeprefix("/profiles/").removesuffix("/inspect").strip("/") return HTTPStatus.OK, profile_inspect(profile, root) + if path.startswith("/profiles/") and path.endswith("/validate"): + profile = path.removeprefix("/profiles/").removesuffix("/validate").strip("/") + payload = profile_validate(profile, root) + return (HTTPStatus.OK if payload["ok"] else HTTPStatus.BAD_REQUEST), payload + if path.startswith("/profiles/") and path.endswith("/graph"): + profile = path.removeprefix("/profiles/").removesuffix("/graph").strip("/") + graph_format = _first(query, "format") or "json" + return HTTPStatus.OK, profile_graph(profile, root, output_format=graph_format) return HTTPStatus.NOT_FOUND, { "ok": False, "error": { diff --git a/src/info_tech_canon/cli.py b/src/info_tech_canon/cli.py index 38c92ba..9bc18c9 100644 --- a/src/info_tech_canon/cli.py +++ b/src/info_tech_canon/cli.py @@ -19,7 +19,9 @@ from .service import ( list_models, list_standards, list_views, + profile_graph, profile_inspect, + profile_validate, read_view, validate_canon, write_validation_report, @@ -81,6 +83,13 @@ def build_parser() -> argparse.ArgumentParser: profile_inspect_cmd = profile_sub.add_parser("inspect", help="Inspect a profile") profile_inspect_cmd.add_argument("profile") profile_inspect_cmd.set_defaults(handler=_profile_inspect) + profile_validate_cmd = profile_sub.add_parser("validate", help="Validate a profile") + profile_validate_cmd.add_argument("profile") + profile_validate_cmd.set_defaults(handler=_profile_validate) + profile_graph_cmd = profile_sub.add_parser("graph", help="Export a profile graph") + profile_graph_cmd.add_argument("profile") + profile_graph_cmd.add_argument("--format", choices=["json", "mermaid"], default="json") + profile_graph_cmd.set_defaults(handler=_profile_graph) api = sub.add_parser("api", help="Run the read-only local API") api.add_argument("--host", default="127.0.0.1") @@ -168,6 +177,14 @@ def _profile_inspect(args: argparse.Namespace) -> dict[str, Any]: return profile_inspect(args.profile, _root(args)) +def _profile_validate(args: argparse.Namespace) -> dict[str, Any]: + return profile_validate(args.profile, _root(args)) + + +def _profile_graph(args: argparse.Namespace) -> dict[str, Any]: + return profile_graph(args.profile, _root(args), output_format=args.format) + + def _api(args: argparse.Namespace) -> dict[str, Any]: serve(host=args.host, port=args.port, root=_root(args)) return {} diff --git a/src/info_tech_canon/profiles.py b/src/info_tech_canon/profiles.py new file mode 100644 index 0000000..dab72cb --- /dev/null +++ b/src/info_tech_canon/profiles.py @@ -0,0 +1,402 @@ +from __future__ import annotations + +from dataclasses import asdict +from pathlib import Path +from typing import Any + +import yaml + +from .bench import export_mermaid, relationship_summary + + +REQUIRED_SMALL_SAAS_KINDS = { + "service", + "system", + "tenant", + "user", + "team", + "dataset", + "deployment", + "task", + "policy", + "control", + "evidence", + "incident", +} + + +def inspect_profile(context: Any, profile: str) -> dict[str, Any]: + definition = load_profile_definition(context, profile) + records = profile_artifact_records(context, profile) + return { + "ok": True, + "profile": definition, + "path": str(profile_path(context, profile)), + "artifact_count": len(records), + "artifacts": [artifact.to_dict() for artifact in records], + } + + +def validate_profile(context: Any, profile: str) -> dict[str, Any]: + definition = load_profile_definition(context, profile) + records = profile_artifact_records(context, profile) + payloads = load_profile_artifacts(context, records) + errors: list[dict[str, Any]] = [] + warnings: list[dict[str, Any]] = [] + + _check_profile_definition(profile, definition, records, errors) + _check_required_artifact_kinds(profile, payloads, errors) + _check_artifact_payloads(profile, payloads, errors) + _check_service_ownership(payloads, errors) + _check_tenant_namespace_separation(payloads, errors) + _check_user_management_and_access(payloads, errors) + _check_governance_evidence(payloads, errors) + + return { + "ok": not errors, + "profile": profile, + "errors": errors, + "warnings": warnings, + "details": { + "artifact_count": len(records), + "payload_count": len(payloads), + "kinds": _kind_counts(payloads), + }, + } + + +def profile_graph( + context: Any, + profile: str, + *, + output_format: str = "json", +) -> dict[str, Any]: + records = profile_artifact_records(context, profile) + record_ids = {artifact.id for artifact in records} + include_ids = set(record_ids) + for artifact in records: + for relationship in artifact.relationships: + target = relationship.get("target") + if isinstance(target, str): + include_ids.add(target) + artifacts = [ + artifact for artifact in context.infospace.artifacts if artifact.id in include_ids + ] + summary = relationship_summary(artifacts) + if output_format == "mermaid": + return {"ok": True, "profile": profile, "format": "mermaid", "graph": export_mermaid(summary)} + if output_format != "json": + raise ValueError(f"Unsupported graph format: {output_format}") + return { + "ok": True, + "profile": profile, + "format": "json", + "graph": { + "node_count": summary.node_count, + "edge_count": summary.edge_count, + "nodes": summary.nodes, + "edges": [asdict(edge) for edge in summary.edges], + "relationship_types": summary.relationship_types, + }, + } + + +def profile_path(context: Any, profile: str) -> Path: + return context.infospace_root / "profiles" / profile / "profile.yaml" + + +def load_profile_definition(context: Any, profile: str) -> dict[str, Any]: + path = profile_path(context, profile) + with path.open("r", encoding="utf-8") as handle: + data = yaml.safe_load(handle) or {} + if not isinstance(data, dict): + raise ValueError(f"Profile must be a YAML mapping: {profile}") + return data + + +def profile_artifact_records(context: Any, profile: str) -> list[Any]: + return [ + artifact + for artifact in context.infospace.artifacts + if artifact.id == f"profile/{profile}" + or artifact.provenance.get("profile") == profile + ] + + +def load_profile_artifacts(context: Any, records: list[Any]) -> dict[str, dict[str, Any]]: + payloads: dict[str, dict[str, Any]] = {} + for artifact in records: + path = context.infospace_root / artifact.path + with path.open("r", encoding="utf-8") as handle: + data = yaml.safe_load(handle) or {} + if isinstance(data, dict): + payloads[artifact.id] = data + return payloads + + +def _check_profile_definition( + profile: str, + definition: dict[str, Any], + records: list[Any], + errors: list[dict[str, Any]], +) -> None: + for field in ("id", "title", "scope", "conformance_level", "required_standards"): + if not definition.get(field): + errors.append( + { + "code": "missing_profile_field", + "profile": profile, + "field": field, + } + ) + declared = set(definition.get("artifact_ids") or []) + actual = {artifact.id for artifact in records} + missing = sorted(declared - actual) + for artifact_id in missing: + errors.append( + { + "code": "missing_profile_artifact_record", + "profile": profile, + "artifact_id": artifact_id, + } + ) + + +def _check_required_artifact_kinds( + profile: str, + payloads: dict[str, dict[str, Any]], + errors: list[dict[str, Any]], +) -> None: + kinds = {payload.get("kind") for payload in payloads.values()} + for kind in sorted(REQUIRED_SMALL_SAAS_KINDS - kinds): + errors.append( + { + "code": "missing_required_profile_kind", + "profile": profile, + "kind": kind, + } + ) + + +def _check_artifact_payloads( + profile: str, + payloads: dict[str, dict[str, Any]], + errors: list[dict[str, Any]], +) -> None: + ids = set(payloads) + for artifact_id, payload in payloads.items(): + for field in ("id", "kind", "title", "profile"): + if not payload.get(field): + errors.append( + { + "code": "missing_profile_artifact_field", + "profile": profile, + "artifact_id": artifact_id, + "field": field, + } + ) + if payload.get("profile") not in {profile, None}: + errors.append( + { + "code": "profile_artifact_profile_mismatch", + "profile": profile, + "artifact_id": artifact_id, + "value": payload.get("profile"), + } + ) + for relationship in payload.get("relationships") or []: + target = relationship.get("target") + if target and target not in ids: + errors.append( + { + "code": "profile_relationship_target_not_in_payloads", + "profile": profile, + "artifact_id": artifact_id, + "target": target, + } + ) + + +def _check_service_ownership( + payloads: dict[str, dict[str, Any]], + errors: list[dict[str, Any]], +) -> None: + service = _one_kind(payloads, "service") + if not service: + return + owner_team = service.get("owner_team") + if not _exists_kind(payloads, owner_team, "team"): + errors.append( + { + "code": "invalid_service_owner_team", + "artifact_id": service.get("id"), + "owner_team": owner_team, + } + ) + team = payloads.get(str(owner_team)) + if team and not _exists_kind(payloads, team.get("owner_user"), "user"): + errors.append( + { + "code": "invalid_team_owner_user", + "artifact_id": team.get("id"), + "owner_user": team.get("owner_user"), + } + ) + + +def _check_tenant_namespace_separation( + payloads: dict[str, dict[str, Any]], + errors: list[dict[str, Any]], +) -> None: + tenants = [payload for payload in payloads.values() if payload.get("kind") == "tenant"] + namespaces = [tenant.get("namespace") for tenant in tenants] + if len(namespaces) != len(set(namespaces)): + errors.append({"code": "duplicate_tenant_namespace"}) + deployment = _one_kind(payloads, "deployment") + if deployment: + if deployment.get("namespace_strategy") != "namespace-per-tenant": + errors.append( + { + "code": "invalid_namespace_strategy", + "artifact_id": deployment.get("id"), + "namespace_strategy": deployment.get("namespace_strategy"), + } + ) + tenant_namespaces = deployment.get("tenant_namespaces") or {} + for tenant in tenants: + if tenant.get("id") not in tenant_namespaces: + errors.append( + { + "code": "tenant_missing_deployment_namespace", + "tenant_id": tenant.get("id"), + "deployment_id": deployment.get("id"), + } + ) + dataset = _one_kind(payloads, "dataset") + if dataset: + if dataset.get("tenant_scope") != "per-tenant": + errors.append( + { + "code": "dataset_not_per_tenant", + "artifact_id": dataset.get("id"), + } + ) + tenant_ids = set(dataset.get("tenant_ids") or []) + for tenant in tenants: + if tenant.get("id") not in tenant_ids: + errors.append( + { + "code": "tenant_missing_dataset_partition", + "tenant_id": tenant.get("id"), + "dataset_id": dataset.get("id"), + } + ) + + +def _check_user_management_and_access( + payloads: dict[str, dict[str, Any]], + errors: list[dict[str, Any]], +) -> None: + evidence_ids = _ids_by_kind(payloads, "evidence") + policy_ids = _ids_by_kind(payloads, "policy") + tenant_ids = _ids_by_kind(payloads, "tenant") + for user in [payload for payload in payloads.values() if payload.get("kind") == "user"]: + if not user.get("teams"): + errors.append( + { + "code": "user_missing_team_membership", + "artifact_id": user.get("id"), + } + ) + grants = user.get("access_grants") or [] + if not grants: + errors.append( + { + "code": "user_missing_access_grant", + "artifact_id": user.get("id"), + } + ) + for grant in grants: + if grant.get("tenant_id") not in tenant_ids: + errors.append( + { + "code": "access_grant_missing_tenant", + "artifact_id": user.get("id"), + "tenant_id": grant.get("tenant_id"), + } + ) + if grant.get("policy_id") not in policy_ids: + errors.append( + { + "code": "access_grant_missing_policy", + "artifact_id": user.get("id"), + "policy_id": grant.get("policy_id"), + } + ) + if grant.get("evidence_id") not in evidence_ids: + errors.append( + { + "code": "access_grant_missing_evidence", + "artifact_id": user.get("id"), + "evidence_id": grant.get("evidence_id"), + } + ) + + +def _check_governance_evidence( + payloads: dict[str, dict[str, Any]], + errors: list[dict[str, Any]], +) -> None: + evidence_ids = _ids_by_kind(payloads, "evidence") + for kind in ("policy", "control", "incident", "service", "dataset", "deployment", "task"): + for payload in [item for item in payloads.values() if item.get("kind") == kind]: + ids = set(payload.get("evidence_ids") or []) + if not ids: + errors.append( + { + "code": "artifact_missing_evidence", + "artifact_id": payload.get("id"), + "kind": kind, + } + ) + for evidence_id in ids: + if evidence_id not in evidence_ids: + errors.append( + { + "code": "artifact_missing_evidence_target", + "artifact_id": payload.get("id"), + "evidence_id": evidence_id, + } + ) + + +def _kind_counts(payloads: dict[str, dict[str, Any]]) -> dict[str, int]: + counts: dict[str, int] = {} + for payload in payloads.values(): + kind = str(payload.get("kind") or "unknown") + counts[kind] = counts.get(kind, 0) + 1 + return dict(sorted(counts.items())) + + +def _one_kind(payloads: dict[str, dict[str, Any]], kind: str) -> dict[str, Any] | None: + for payload in payloads.values(): + if payload.get("kind") == kind: + return payload + return None + + +def _exists_kind( + payloads: dict[str, dict[str, Any]], + artifact_id: Any, + kind: str, +) -> bool: + payload = payloads.get(str(artifact_id)) + return bool(payload and payload.get("kind") == kind) + + +def _ids_by_kind(payloads: dict[str, dict[str, Any]], kind: str) -> set[str]: + return { + str(payload["id"]) + for payload in payloads.values() + if payload.get("kind") == kind and payload.get("id") + } diff --git a/src/info_tech_canon/service.py b/src/info_tech_canon/service.py index e65fb8e..5b397c1 100644 --- a/src/info_tech_canon/service.py +++ b/src/info_tech_canon/service.py @@ -6,9 +6,8 @@ from pathlib import Path import json from typing import Any -import yaml - from . import generation +from . import profiles from .bench import ( Infospace, KnowledgeArtifact, @@ -230,15 +229,53 @@ def profile_inspect( f"Profile not found: {profile}", {"profile": profile, "path": str(profile_path)}, ) - with profile_path.open("r", encoding="utf-8") as handle: - data = yaml.safe_load(handle) or {} - if not isinstance(data, dict): + try: + return profiles.inspect_profile(context, profile) + except ValueError as exc: raise CanonServiceError( "invalid_profile", f"Profile must be a YAML mapping: {profile}", {"profile": profile, "path": str(profile_path)}, + ) from exc + + +def profile_validate( + profile: str, + root: Path | str | None = None, +) -> dict[str, Any]: + context = load_context(root) + profile_path = context.infospace_root / "profiles" / profile / "profile.yaml" + if not profile_path.is_file(): + raise CanonServiceError( + "missing_profile", + f"Profile not found: {profile}", + {"profile": profile, "path": str(profile_path)}, ) - return {"ok": True, "profile": data, "path": str(profile_path)} + return profiles.validate_profile(context, profile) + + +def profile_graph( + profile: str, + root: Path | str | None = None, + *, + output_format: str = "json", +) -> dict[str, Any]: + context = load_context(root) + profile_path = context.infospace_root / "profiles" / profile / "profile.yaml" + if not profile_path.is_file(): + raise CanonServiceError( + "missing_profile", + f"Profile not found: {profile}", + {"profile": profile, "path": str(profile_path)}, + ) + try: + return profiles.profile_graph(context, profile, output_format=output_format) + except ValueError as exc: + raise CanonServiceError( + "unsupported_graph_format", + str(exc), + {"supported": ["json", "mermaid"]}, + ) from exc def generate_indexes(root: Path | str | None = None) -> dict[str, Any]: diff --git a/tests/test_api.py b/tests/test_api.py index f230c3f..7a5c6c0 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -39,3 +39,11 @@ def test_api_route_reads_generated_view(tmp_path) -> None: assert payload["ok"] is True assert payload["generated"] is True assert "# By Standard" in payload["content"] + + +def test_api_route_validates_small_saas_profile() -> None: + status, payload = _route("/profiles/small-saas/validate", {}, None) + + assert status == HTTPStatus.OK + assert payload["ok"] is True + assert payload["details"]["artifact_count"] == 14 diff --git a/tests/test_cli.py b/tests/test_cli.py index c3de3d1..3de0188 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -11,11 +11,11 @@ def test_cli_inspect_emits_json(capsys) -> None: assert exit_code == 0 payload = json.loads(capsys.readouterr().out) assert payload["ok"] is True - assert payload["infospace"]["artifact_count"] == 15 + assert payload["infospace"]["artifact_count"] == 29 def test_cli_missing_profile_uses_structured_error(capsys) -> None: - exit_code = main(["profile", "inspect", "small-saas"]) + exit_code = main(["profile", "inspect", "missing-profile"]) assert exit_code == 2 payload = json.loads(capsys.readouterr().out) @@ -23,6 +23,15 @@ def test_cli_missing_profile_uses_structured_error(capsys) -> None: assert payload["error"]["code"] == "missing_profile" +def test_cli_small_saas_profile_validate(capsys) -> None: + exit_code = main(["profile", "validate", "small-saas"]) + + assert exit_code == 0 + payload = json.loads(capsys.readouterr().out) + assert payload["ok"] is True + assert payload["details"]["kinds"]["service"] == 1 + + def test_cli_index_generates_views(capsys, tmp_path) -> None: root = tmp_path / "infospace" shutil.copytree(DEFAULT_INFOSPACE_ROOT, root) diff --git a/tests/test_service.py b/tests/test_service.py index d9f71ea..e84b8e6 100644 --- a/tests/test_service.py +++ b/tests/test_service.py @@ -6,6 +6,8 @@ from info_tech_canon.service import ( inspect_canon, list_models, list_standards, + profile_graph, + profile_validate, validate_canon, ) from info_tech_canon.service import DEFAULT_INFOSPACE_ROOT @@ -17,10 +19,12 @@ def test_inspect_canon_counts_artifact_kinds() -> None: assert payload["ok"] is True assert payload["infospace"]["slug"] == "canon" - assert payload["infospace"]["artifact_count"] == 15 + assert payload["infospace"]["artifact_count"] == 29 assert payload["infospace"]["kinds"] == { "kernel": 2, "model": 11, + "profile": 1, + "profile-artifact": 13, "standard": 2, } @@ -36,17 +40,33 @@ 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 + assert payload["details"]["artifact_count"] == 29 def test_graph_exports_relationship_summary() -> None: payload = artifact_graph() assert payload["ok"] is True - assert payload["graph"]["node_count"] == 15 + assert payload["graph"]["node_count"] == 29 assert payload["graph"]["edge_count"] > 15 +def test_small_saas_profile_validates() -> None: + payload = profile_validate("small-saas") + + assert payload["ok"] is True + assert payload["errors"] == [] + assert payload["details"]["kinds"]["tenant"] == 2 + + +def test_small_saas_profile_graph_exports_slice() -> None: + payload = profile_graph("small-saas") + + assert payload["ok"] is True + assert payload["profile"] == "small-saas" + assert "small-saas/service/billing-portal" in payload["graph"]["nodes"] + + def test_generators_write_expected_assets(tmp_path) -> None: root = tmp_path / "infospace" shutil.copytree(DEFAULT_INFOSPACE_ROOT, root) diff --git a/workplans/ITC-WP-0004-small-saas-profile-proof.md b/workplans/ITC-WP-0004-small-saas-profile-proof.md index 33e3827..cc0fc77 100644 --- a/workplans/ITC-WP-0004-small-saas-profile-proof.md +++ b/workplans/ITC-WP-0004-small-saas-profile-proof.md @@ -4,7 +4,7 @@ type: workplan title: "Small SaaS Profile Proof" domain: canon repo: info-tech-canon -status: proposed +status: finished priority: high created: "2026-05-23" updated: "2026-05-23" @@ -35,7 +35,7 @@ governance, observability, and work tracking. ```task id: ITC-WP-0004-T01 -status: todo +status: done priority: high state_hub_task_id: "21801d10-2ec9-45b9-bb7a-632f251075d5" ``` @@ -48,7 +48,7 @@ state_hub_task_id: "21801d10-2ec9-45b9-bb7a-632f251075d5" ```task id: ITC-WP-0004-T02 -status: todo +status: done priority: high state_hub_task_id: "92c860ca-174d-4d31-b11c-66750a2d3588" ``` @@ -62,7 +62,7 @@ state_hub_task_id: "92c860ca-174d-4d31-b11c-66750a2d3588" ```task id: ITC-WP-0004-T03 -status: todo +status: done priority: high state_hub_task_id: "b342257f-beef-4ec6-b116-b9f8798df10c" ``` @@ -77,7 +77,7 @@ state_hub_task_id: "b342257f-beef-4ec6-b116-b9f8798df10c" ```task id: ITC-WP-0004-T04 -status: todo +status: done priority: medium state_hub_task_id: "58a85249-be34-497d-a19c-96b6b343b076" ``` @@ -93,3 +93,16 @@ state_hub_task_id: "58a85249-be34-497d-a19c-96b6b343b076" - The proof produces useful graph and JSON outputs. - The result is suitable as the baseline for user-engine and railiance-fabric evaluation. + +## Implementation Notes + +- Added `infospace/profiles/small-saas/profile.yaml` with scope, + assumptions, required standards, concept obligations, validation rules, and + demo commands. +- Added connected profile artifacts for service, system, tenants, user, team, + dataset, deployment, task, policy, control, evidence, and incident. +- Registered the profile and examples in `infospace/artifacts/index.yaml` so + the graph and global validation include the proof. +- Added `profile validate` and `profile graph` service, CLI, and API support. +- Added a profile report and example command fixture suitable for downstream + user-engine and railiance-fabric evaluation work. diff --git a/workplans/index.yaml b/workplans/index.yaml index 2549f62..ae527ff 100644 --- a/workplans/index.yaml +++ b/workplans/index.yaml @@ -56,7 +56,7 @@ workplans: - id: ITC-WP-0004 title: Small SaaS Profile Proof - status: proposed + status: finished priority: high path: workplans/ITC-WP-0004-small-saas-profile-proof.md depends_on: