Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .gds/bundle.lock.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ bundle:
version: "0.4.0-dev"
release_sequence: 0
channel: "development"
source_tree_digest: "sha256:4983ada020ddf83bc417830cd692d0abc0c804bb00cd4da7e0bd3539699d3c6f"
digest: "sha256:3198ff9cf30f09b88230e9e5772e658760d1bce3f374914ba8cf80e0ca4b2c22"
source_tree_digest: "sha256:16b3bc2c9e627bc1068197e652c41ecc3529385e8c97356c4d21262621e4be23"
digest: "sha256:7bbf0c6aad8a1628d64d4ded7f5bda6fea8600a0f858de255e43188a9cd88849"

projection:
input_digest: "sha256:70a05ee495fb5fc571db71d52f16739575e468def2c5bf13781438806b882c84"
output_digest: "sha256:58f8443e4b6dced0eb87255b17a207039b1bcc8dd86569c4cbfb0a14faa2ab37"
input_digest: "sha256:4779d11659c59942a16e3169848fac07866524eaf312a2d33316ffe62acf1374"
output_digest: "sha256:39450ad1421046ffd3807d8c72cf4ab25d08f4f29e70ed11e6919bd7f6c57789"
files:
- path: ".gds/compiled-policy.json"
digest: "sha256:78d09606bb4168d74bce1f50ab62b46a7ded34652c6b23af1badfd26dd060e94"
- path: ".github/workflows/gds-ci.yml"
digest: "sha256:57f868a1172afeaf4844d95fb9b0eece46bf07c3d3101cd9158f82e0692d3a37"
digest: "sha256:1481d86ea53eb7f9ef7ff77fbfb79ced779be7897b6190d14d98e30d66674801"
4 changes: 2 additions & 2 deletions .github/workflows/gds-ci.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# GENERATED FILE - DO NOT EDIT DIRECTLY
# generator: gds
# bundle: 0.4.0-dev
# source-tree-digest: sha256:4983ada020ddf83bc417830cd692d0abc0c804bb00cd4da7e0bd3539699d3c6f
# input-digest: sha256:70a05ee495fb5fc571db71d52f16739575e468def2c5bf13781438806b882c84
# source-tree-digest: sha256:16b3bc2c9e627bc1068197e652c41ecc3529385e8c97356c4d21262621e4be23
# input-digest: sha256:4779d11659c59942a16e3169848fac07866524eaf312a2d33316ffe62acf1374
# output-digest: sha256:01fb4854784be9e4564bcc84e70786484b370879be5e5ab1dd49f8b73ea2dea4
# edit-source:
# - .gds/repository.yaml
Expand Down
39 changes: 39 additions & 0 deletions core/estate/index_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,42 @@ func TestIdentityIndexAcceptsDeclaredPackageConsumption(t *testing.T) {
t.Fatalf("declared package consumption must pass, got %+v", findings)
}
}

// A generated-from edge names the repository that renders this one. It is not a
// dependency: nothing is vendored, pinned or executed from the generator, so it
// must never appear as a consumer edge and must not demand a module.consumption
// declaration the way the three consumer types do.
func TestBuildIdentityIndexRecordsGeneratedFromWithoutDerivingAConsumer(t *testing.T) {
generatorID := "repo_01JEXAMPZ0000000000000000G"
renderedID := "repo_01JEXAMPZ0000000000000000H"
generator := indexedAnchor(generatorID, 10, "owner", "setup-systems")
rendered := indexedAnchor(renderedID, 11, "owner", "claude-setup-system")
rendered.Relationships = []domain.Relationship{{
Type: "generated-from", Target: generatorID,
}}
input := []IndexedRepository{{Path: "/rendered", Anchor: rendered}, {Path: "/generator", Anchor: generator}}
index, findings := BuildIdentityIndex(input, true)
if len(findings) != 0 {
t.Fatalf("generated-from must not produce findings: %+v", findings)
}
if len(index.Consumers) != 0 {
t.Fatalf("generated-from must not derive a consumer edge: %+v", index.Consumers)
}
if len(index.Relationships) != 1 || index.Relationships[0].Type != "generated-from" ||
index.Relationships[0].Source != renderedID || index.Relationships[0].Target != generatorID {
t.Fatalf("generated-from edge not recorded: %+v", index.Relationships)
}
}

// The edge is declared on the rendered repository, so an absent generator is a
// missing target like any other typed relationship rather than a silent pass.
func TestBuildIdentityIndexRejectsGeneratedFromWithAbsentGenerator(t *testing.T) {
renderedID := "repo_01JEXAMPZ0000000000000000H"
missingID := "repo_01JEXAMPZ0000000000000000G"
rendered := indexedAnchor(renderedID, 11, "owner", "claude-setup-system")
rendered.Relationships = []domain.Relationship{{Type: "generated-from", Target: missingID}}
_, findings := BuildIdentityIndex([]IndexedRepository{{Path: "/rendered", Anchor: rendered}}, true)
if len(findings) != 1 || findings[0].Code != "GDS_IDENTITY_INDEX_TARGET_MISSING" {
t.Fatalf("expected one missing-target finding, got %+v", findings)
}
}
14 changes: 14 additions & 0 deletions docs/adr/0005-typed-relationships.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,26 @@ Relationships are explicit typed edges, including:

- portfolio membership;
- fork-of;
- generated-from;
- git-submodule consumer;
- package consumer;
- device checkout;
- worktree;
- embedded context source.

`generated-from` is declared on the rendered repository and names the repository
that renders it. It is not a dependency edge: nothing is vendored, pinned or
executed from the generator, so it derives no consumer and requires no
`module.consumption` declaration. Declaring it in the other direction was
rejected for the same reason a parent field was: one generator would carry a
list of every tree it renders, and that list is a second copy of a fact the
renderer already owns.

The direction also decides whether the fact can be checked. A rendered tree
asserting "I came from X" can be verified against the generator's own output; a
generator asserting "I produce these seven" has nothing local to check it
against, and drifts exactly when a tree is added or removed.

Provider owner/name and local paths are mutable locators with alias/history
records.

Expand Down
1 change: 1 addition & 0 deletions schemas/v1/repository.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,7 @@
"package-consumer",
"workflow-module-consumer",
"fork-of",
"generated-from",
"device-checkout",
"worktree",
"embedded-context-source"
Expand Down
6 changes: 6 additions & 0 deletions tests/fixtures/schemas/v1/cases.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@
"path": "valid-audit-snapshot.json",
"valid": true
},
{
"id": "valid-generated-repository",
"schema": "repository",
"path": "valid-generated-repository.yaml",
"valid": true
},
{
"id": "valid-repository",
"schema": "repository",
Expand Down
57 changes: 57 additions & 0 deletions tests/fixtures/schemas/v1/valid-generated-repository.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
schema_version: 1

repository:
id: "repo_01JEXAMPZ0000000000000000C"
display_name: "example-rendered-tree"
roles:
- "project"
lifecycle: "active"

provider:
type: "github"
installation: "installation:github-personal"
repository_id: 123456789
owner: "example-user"
name: "example-rendered-tree"

classification:
portfolios:
- "portfolio:personal-projects"
visibility_contract: "private"
data_classification: "private"

policy:
profiles:
- "repository-default"
rollout_ring: "standard"

git:
default_branch: "main"
integration: "pull-request"
branch_model: "task-branches"
handoff_pr: "preferred"
cleanup: "merged-only"

verification:
commands:
test:
- "python -m unittest"
required:
- "test"

agent:
context_profile: "project-default"
generated_agents: true
serena:
enabled: true
provenance_required: true

release:
mode: "none"

# A rendered tree names the repository that renders it. The edge is declared
# here rather than on the generator so that adding or removing a tree never
# requires a commit to the generator's own anchor.
relationships:
- type: "generated-from"
target: "repo_01JEXAMPZ0000000000000000D"