From 3306c7aefff722e796826ebc3522ae3647d47f09 Mon Sep 17 00:00:00 2001 From: Adrian Webb Date: Sun, 20 Sep 2026 23:57:41 -0400 Subject: [PATCH 1/6] Adopt canonical governed knowledge content --- .fixtures/treeseed-fixtures | 2 +- src/content/content.ts | 17 ++++++++++++----- src/content/portable-content-schema.ts | 8 +++++++- .../t/[teamSlug]/books/[bookSlug]/index.astro | 7 +------ 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/.fixtures/treeseed-fixtures b/.fixtures/treeseed-fixtures index 6c4c88a..7413133 160000 --- a/.fixtures/treeseed-fixtures +++ b/.fixtures/treeseed-fixtures @@ -1 +1 @@ -Subproject commit 6c4c88af280a871def54260b037ca99184165e0d +Subproject commit 74131336841325f417003bb9dd8c4b23e1b478d6 diff --git a/src/content/content.ts b/src/content/content.ts index 2cc0057..6cf03b5 100644 --- a/src/content/content.ts +++ b/src/content/content.ts @@ -135,7 +135,17 @@ function resolveDocsCollectionProvider( extend: withPortableContentValidation('knowledge', z.object({ schemaVersion: z.literal(KNOWLEDGE_PAGE_SCHEMA_VERSION), id: z.string(), - bookId: z.string(), + projectId: z.string(), + bookRef: z.object({ + store: z.literal('treedx'), + model: z.literal('book'), + id: z.string(), + revision: z.number().int().positive(), + digest: z.string(), + repository: z.string(), + commit: z.string().optional(), + path: z.string(), + }).passthrough(), slug: z.string(), summary: z.string(), status: z.enum(KNOWLEDGE_STATUSES), @@ -158,7 +168,7 @@ function resolveDocsCollectionProvider( actionIds: z.array(z.string()).default([]), keywords: z.array(z.string()).default([]), documentationUrls: z.array(z.string()).default([]), - })), + }).transform((value) => ({ ...value, bookId: value.bookRef.id }))), }), }; } @@ -226,9 +236,6 @@ export function createCollections(tenantConfig: TenantConfig, { docsLoader, docs agent_context_query_sets: ['agent_context_query_set','agent-context-query-sets'], agent_instruction_templates: ['agent_instruction_template','agent-instruction-templates'], discussion_topics: ['discussion_topic','discussion-topics'], - assignment_plans: ['assignment_plan','assignment-plans'], - assignment_statuses: ['assignment_status','assignment-statuses'], - assignment_summaries: ['assignment_summary','assignment-summaries'], agent_evaluations: ['agent_evaluation','agent-evaluations'], } as const; for (const [collection,[model,directory]] of Object.entries(operationalCollections)) { diff --git a/src/content/portable-content-schema.ts b/src/content/portable-content-schema.ts index 3178c6b..16225cb 100644 --- a/src/content/portable-content-schema.ts +++ b/src/content/portable-content-schema.ts @@ -17,7 +17,13 @@ function issuePath(field: string | undefined) { export function withPortableContentValidation(model: PortableContentModel, schema: TSchema) { return z.any().superRefine((value, context) => { - const result = validatePortableContentData(model, value); + const record = value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; + const { prev: _prev, next: _next, tableOfContents: _tableOfContents, sidebar: _sidebar, ...content } = record; + // Astro validates frontmatter before exposing the parsed Markdown body. + // The complete body is validated at the TreeDX/API publication boundary. + const candidate = model === 'knowledge' && content.body === undefined + ? { ...content, body: '[parsed Markdown body]' } : content; + const result = validatePortableContentData(model, candidate); for (const diagnostic of result.diagnostics) context.addIssue({ code: z.ZodIssueCode.custom, path: issuePath(inputField(diagnostic.field, value)), diff --git a/src/pages/t/[teamSlug]/books/[bookSlug]/index.astro b/src/pages/t/[teamSlug]/books/[bookSlug]/index.astro index 4fe80f4..8eebb69 100644 --- a/src/pages/t/[teamSlug]/books/[bookSlug]/index.astro +++ b/src/pages/t/[teamSlug]/books/[bookSlug]/index.astro @@ -17,12 +17,7 @@ const pages = federated?.book ? federated.pages : book ? pagesForBook(library.pa if ((federated as any)?.unavailable) Astro.response.status = 503; else if (!book || (!federated?.book && teamSlug !== 'treeseed')) Astro.response.status = 404; const overview = book ? { - schemaVersion: 'treeseed.knowledge-page/v1', id: `${book.data.id}.overview`, bookId: book.data.id, - slug: 'overview', title: book.data.title, summary: book.data.summary, status: 'published', - visibility: book.data.visibility, order: 0, groupIds: book.data.groupIds ?? [], contributors: [], - relatedBookIds: book.data.relatedBookIds ?? [], relatedKnowledgeIds: [], capabilityIds: [], - guaranteeIds: [], - routePatterns: [], resourceTypes: ['book'], actionIds: [], keywords: book.data.groupIds ?? [], documentationUrls: [], + title: book.data.title, summary: book.data.summary, description: book.data.description, } : null; --- From 989d4befd4e66da1a03d2c8ac8f6500e6afa611a Mon Sep 17 00:00:00 2001 From: Adrian Webb Date: Mon, 21 Sep 2026 00:25:17 -0400 Subject: [PATCH 2/6] Pin canonical governed content contracts --- package-lock.json | 195 ++++++---------------------------------------- package.json | 2 +- 2 files changed, 26 insertions(+), 171 deletions(-) diff --git a/package-lock.json b/package-lock.json index 81c4ead..69df4c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "@astrojs/sitemap": "3.7.0", "@astrojs/starlight": "0.37.6", "@tailwindcss/vite": "^4.1.4", - "@treeseed/sdk": ">=0.13.0-rc.82 <0.14.0", + "@treeseed/sdk": "0.13.0-rc.122", "@treeseed/ui": ">=0.12.18-rc.11 <0.14.0", "astro": "^5.6.1", "esbuild": "^0.28.0", @@ -2566,9 +2566,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2585,9 +2582,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2604,9 +2598,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2623,9 +2614,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2642,9 +2630,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2661,9 +2646,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2680,9 +2662,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2699,9 +2678,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2718,9 +2694,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2743,9 +2716,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2768,9 +2738,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2793,9 +2760,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2818,9 +2782,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2843,9 +2804,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2868,9 +2826,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2893,9 +2848,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -3873,9 +3825,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4963,9 +4912,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4979,9 +4925,6 @@ "cpu": [ "arm" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4995,9 +4938,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5011,9 +4951,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5027,9 +4964,6 @@ "cpu": [ "loong64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5043,9 +4977,6 @@ "cpu": [ "loong64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5059,9 +4990,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5075,9 +5003,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5091,9 +5016,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5107,9 +5029,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5123,9 +5042,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5139,9 +5055,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5155,9 +5068,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5465,9 +5375,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5484,9 +5391,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5503,9 +5407,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5522,9 +5423,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5670,13 +5568,14 @@ } }, "node_modules/@treeseed/sdk": { - "version": "0.13.0-rc.82", - "resolved": "https://registry.npmjs.org/@treeseed/sdk/-/sdk-0.13.0-rc.82.tgz", - "integrity": "sha512-3dkc0jUlxwc5kSlew8MlmlgDxwv4AMfPjXJR6PsexKedEH8s65y0u9TxD9SASd/titmrIX/Nqczk0t/RSq2BmA==", + "version": "0.13.0-rc.122", + "resolved": "https://registry.npmjs.org/@treeseed/sdk/-/sdk-0.13.0-rc.122.tgz", + "integrity": "sha512-JKk6yDA1SDV2rWscnr95XsIGUdOcTYC8Ac6adQXkXArMriSVn0g25IPOntj2l2TK8s83KoKUmPaa9QfXbqg5jg==", "license": "Apache-2.0", "dependencies": { - "@treeseed/treedx": "0.3.0-rc.4", + "@treeseed/treedx": "0.3.0-rc.17", "esbuild": "^0.28.0", + "libsodium-wrappers-sumo": "0.8.4", "typescript": "^5.9.3", "yaml": "^2.8.1", "zod": "^3.25.76" @@ -5686,9 +5585,9 @@ } }, "node_modules/@treeseed/treedx": { - "version": "0.3.0-rc.4", - "resolved": "https://registry.npmjs.org/@treeseed/treedx/-/treedx-0.3.0-rc.4.tgz", - "integrity": "sha512-ZBmY/C30kge84Ns/qzpa/h4XfLXtZMDiyfCJM8WTLHQBU2I5QYrhx2aI32mHk90jQib+SyWfwYMknIip8RHF8A==", + "version": "0.3.0-rc.17", + "resolved": "https://registry.npmjs.org/@treeseed/treedx/-/treedx-0.3.0-rc.17.tgz", + "integrity": "sha512-L1DCxkOZGS2of2IvbSC70baIpGlEuFBpHUhV4oKq53xXARN8dw4st2SFUz7IaxOIRzR9gD/vH12hIPnTFLJ7tA==", "license": "Apache-2.0", "engines": { "node": ">=22" @@ -9214,6 +9113,21 @@ "url": "https://github.com/sponsors/dmonad" } }, + "node_modules/libsodium-sumo": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/libsodium-sumo/-/libsodium-sumo-0.8.4.tgz", + "integrity": "sha512-TMtHShQfVVsaxDygyapvUC3o7YsPgXa/hRWeIgzyFz6w5k/1hirGptCxp1U7XwW3rCskaTTYKgV10v86UiGgNw==", + "license": "ISC" + }, + "node_modules/libsodium-wrappers-sumo": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.8.4.tgz", + "integrity": "sha512-ql7hcgulKZ3ekfa2DGAogcCKsWU0diA/0nArz1CFzh93WQdb46/Kj18ka/Hifq6uA3Ush34Pc6vU/6HXeRwUkg==", + "license": "ISC", + "dependencies": { + "libsodium-sumo": "^0.8.0" + } + }, "node_modules/lightningcss": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", @@ -9350,9 +9264,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -9373,9 +9284,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -9396,9 +9304,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -9419,9 +9324,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -10833,9 +10735,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -10852,9 +10751,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -10871,9 +10767,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -10890,9 +10783,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -10909,9 +10799,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -10928,9 +10815,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -10947,9 +10831,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -10966,9 +10847,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -10985,9 +10863,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -11010,9 +10885,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -11035,9 +10907,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -11060,9 +10929,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -11085,9 +10951,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -11110,9 +10973,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -11135,9 +10995,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -11160,9 +11017,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -13024,6 +12878,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, diff --git a/package.json b/package.json index a859562..297113f 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "@astrojs/sitemap": "3.7.0", "@astrojs/starlight": "0.37.6", "@tailwindcss/vite": "^4.1.4", - "@treeseed/sdk": ">=0.13.0-rc.82 <0.14.0", + "@treeseed/sdk": "0.13.0-rc.122", "@treeseed/ui": ">=0.12.18-rc.11 <0.14.0", "astro": "^5.6.1", "esbuild": "^0.28.0", From 002a42e860a4c8ccc0590dfcbd167809ca9e4757 Mon Sep 17 00:00:00 2001 From: Adrian Webb Date: Mon, 21 Sep 2026 00:36:18 -0400 Subject: [PATCH 3/6] Remove duplicate Core governance contracts --- src/content/governance-schemas.ts | 258 ++---------------- .../governance-content-schemas.test.ts | 40 +-- 2 files changed, 45 insertions(+), 253 deletions(-) diff --git a/src/content/governance-schemas.ts b/src/content/governance-schemas.ts index 288f01a..2c655f6 100644 --- a/src/content/governance-schemas.ts +++ b/src/content/governance-schemas.ts @@ -1,246 +1,48 @@ -import { reference } from 'astro:content'; import { z } from 'astro/zod'; import type { FieldAliasRegistry } from '@treeseed/sdk/field-aliases'; import { preprocessAliasedRecord } from '@treeseed/sdk/field-aliases'; -import { DECISION_MODEL_DEFAULTS, NOTE_MODEL_DEFAULTS, OBJECTIVE_MODEL_DEFAULTS, PAGE_MODEL_DEFAULTS, PROPOSAL_MODEL_DEFAULTS, QUESTION_MODEL_DEFAULTS } from '../utils/configuration/site-config.ts'; -import { PROPOSAL_TYPE_ID_PATTERN } from '@treeseed/sdk/agent-capacity'; +import { PAGE_MODEL_DEFAULTS } from '../utils/configuration/site-config.ts'; import { withPortableContentValidation } from './portable-content-schema.ts'; const statusValues = ['live', 'in progress', 'exploratory', 'planned', 'speculative'] as const; const pageLayoutValues = ['article', 'bridge'] as const; -const questionTypeValues = ['research', 'implementation', 'strategy', 'evaluation', 'knowledge-gap'] as const; -const decisionTypeValues = ['approved', 'rejected', 'deferred', 'request_changes', 'superseded'] as const; -const governanceStatusValues = ['draft', 'open', 'voting', 'accepted', 'rejected', 'no_decision_quorum_failed', 'withdrawn', 'superseded'] as const; -const timeHorizonValues = ['near-term', 'mid-term', 'long-term'] as const; + function withOptionalDefault TSchema }>(schema: TSchema, value: unknown) { return value === undefined ? schema : schema.default(value); } +/** + * Repository-authored site pages retain their presentation schema. Governed + * operational content is validated only by the canonical SDK schemas before + * Astro receives it; Core must not maintain a second proposal/decision model. + */ export function createGovernanceCollectionSchemas() { - const noteFieldAliases: FieldAliasRegistry = { - feedbackKind: { key: 'feedbackKind', aliases: ['feedback_kind'] }, - }; const pageFieldAliases: FieldAliasRegistry = { - pageLayout: { key: 'pageLayout', aliases: ['page_layout'] }, - seoTitle: { key: 'seoTitle', aliases: ['seo_title'] }, - seoDescription: { key: 'seoDescription', aliases: ['seo_description'] }, - }; - - const questionFieldAliases: FieldAliasRegistry = { - questionType: { key: 'questionType', aliases: ['question_type'] }, - primaryContributor: { key: 'primaryContributor', aliases: ['primary_contributor'] }, - relatedObjectives: { key: 'relatedObjectives', aliases: ['related_objectives'] }, - relatedProposals: { key: 'relatedProposals', aliases: ['related_proposals'] }, - relatedBooks: { key: 'relatedBooks', aliases: ['related_books'] }, - }; - - const objectiveFieldAliases: FieldAliasRegistry = { - timeHorizon: { key: 'timeHorizon', aliases: ['time_horizon'] }, - primaryContributor: { key: 'primaryContributor', aliases: ['primary_contributor'] }, - relatedQuestions: { key: 'relatedQuestions', aliases: ['related_questions'] }, - relatedBooks: { key: 'relatedBooks', aliases: ['related_books'] }, - }; - - const proposalFieldAliases: FieldAliasRegistry = { - proposalType: { key: 'proposalType', aliases: ['proposal_type'] }, - primaryContributor: { key: 'primaryContributor', aliases: ['primary_contributor'] }, - relatedObjectives: { key: 'relatedObjectives', aliases: ['related_objectives'] }, - relatedQuestions: { key: 'relatedQuestions', aliases: ['related_questions'] }, - relatedNotes: { key: 'relatedNotes', aliases: ['related_notes'] }, - relatedBooks: { key: 'relatedBooks', aliases: ['related_books'] }, - governanceId: { key: 'governanceId', aliases: ['governance_id'] }, - governanceProviderId: { key: 'governanceProviderId', aliases: ['governance_provider_id'] }, - governancePolicyId: { key: 'governancePolicyId', aliases: ['governance_policy_id'] }, - governanceStatus: { key: 'governanceStatus', aliases: ['governance_status'] }, - proposalVersion: { key: 'proposalVersion', aliases: ['proposal_version'] }, - proposalContentHash: { key: 'proposalContentHash', aliases: ['proposal_content_hash'] }, - votingStartsAt: { key: 'votingStartsAt', aliases: ['voting_starts_at'] }, - votingEndsAt: { key: 'votingEndsAt', aliases: ['voting_ends_at'] }, - canonicalRoute: { key: 'canonicalRoute', aliases: ['canonical_route'] }, - evidenceRefs: { key: 'evidenceRefs', aliases: ['evidence_refs'] }, - contentProvenance: { key: 'contentProvenance', aliases: ['content_provenance'] }, - decisionDependencies: { key: 'decisionDependencies', aliases: ['decision_dependencies'] }, - }; - - const decisionFieldAliases: FieldAliasRegistry = { - decisionType: { key: 'decisionType', aliases: ['decision_type'] }, - primaryContributor: { key: 'primaryContributor', aliases: ['primary_contributor'] }, - relatedObjectives: { key: 'relatedObjectives', aliases: ['related_objectives'] }, - relatedQuestions: { key: 'relatedQuestions', aliases: ['related_questions'] }, - relatedNotes: { key: 'relatedNotes', aliases: ['related_notes'] }, - relatedProposals: { key: 'relatedProposals', aliases: ['related_proposals'] }, - relatedBooks: { key: 'relatedBooks', aliases: ['related_books'] }, - governanceDecisionId: { key: 'governanceDecisionId', aliases: ['governance_decision_id'] }, - governanceProviderId: { key: 'governanceProviderId', aliases: ['governance_provider_id'] }, - sourceProposalGovernanceId: { key: 'sourceProposalGovernanceId', aliases: ['source_proposal_governance_id'] }, - sourceProposalVersion: { key: 'sourceProposalVersion', aliases: ['source_proposal_version'] }, - sourceProposalHash: { key: 'sourceProposalHash', aliases: ['source_proposal_hash'] }, - governanceRule: { key: 'governanceRule', aliases: ['governance_rule'] }, - electorateSnapshot: { key: 'electorateSnapshot', aliases: ['electorate_snapshot'] }, - voteResult: { key: 'voteResult', aliases: ['vote_result'] }, - voterReasons: { key: 'voterReasons', aliases: ['voter_reasons'] }, - decidedAt: { key: 'decidedAt', aliases: ['decided_at'] }, - decisionSnapshotHash: { key: 'decisionSnapshotHash', aliases: ['decision_snapshot_hash'] }, - canonicalRoute: { key: 'canonicalRoute', aliases: ['canonical_route'] }, - }; - - const contributorReference = z.union([reference('people'), reference('agents')]); - + pageLayout: { key: 'pageLayout', aliases: ['page_layout'] }, + seoTitle: { key: 'seoTitle', aliases: ['seo_title'] }, + seoDescription: { key: 'seoDescription', aliases: ['seo_description'] }, + }; const pageSchema = z.preprocess((value) => preprocessAliasedRecord(pageFieldAliases, value), z.object({ - title: z.string(), - description: z.string(), - slug: z.string(), - pageLayout: withOptionalDefault(z.enum(pageLayoutValues), PAGE_MODEL_DEFAULTS.pageLayout), - status: withOptionalDefault(z.enum(statusValues), PAGE_MODEL_DEFAULTS.status), - stage: withOptionalDefault(z.string(), PAGE_MODEL_DEFAULTS.stage), - audience: z.array(z.string()).default(PAGE_MODEL_DEFAULTS.audience ?? []), - summary: z.string(), - updated: z.coerce.date(), - seoTitle: z.string().optional(), - seoDescription: z.string().optional(), - })); - - const noteSchema = z.preprocess((value) => preprocessAliasedRecord(noteFieldAliases, value), z.object({ - id: z.string().optional(), - title: z.string(), - description: z.string(), - date: z.coerce.date(), - status: withOptionalDefault(z.enum(statusValues), NOTE_MODEL_DEFAULTS.status), - groupIds: z.array(z.string()).default([]), - entityRefs: z.array(z.object({ model: z.string(), id: z.string(), commitSha: z.string().optional() }).strict()).default([]), - author: withOptionalDefault(z.string(), NOTE_MODEL_DEFAULTS.author), - feedbackKind: z.enum(['support', 'concern', 'question', 'response']).optional(), - summary: z.string(), - draft: z.boolean().default(NOTE_MODEL_DEFAULTS.draft ?? false), - canonicalRoute: z.string().optional(), - about: z.array(z.string()).default([]), - relatedObjectives: z.array(z.string()).default([]), - relatedQuestions: z.array(z.string()).default([]), - relatedProposals: z.array(z.string()).default([]), - relatedDecisions: z.array(z.string()).default([]), - relatedBooks: z.array(z.string()).default([]), - audiences: z.object({ - primary: z.array(z.string()).default([]), - secondary: z.array(z.string()).default([]), - excluded: z.array(z.string()).default([]), - }).optional(), - })); - - const questionSchema = z.preprocess((value) => preprocessAliasedRecord(questionFieldAliases, value), z.object({ - title: z.string(), - description: z.string(), - date: z.coerce.date(), - status: withOptionalDefault(z.enum(statusValues), QUESTION_MODEL_DEFAULTS.status), - groupIds: z.array(z.string()).default([]), - entityRefs: z.array(z.object({ model: z.string(), id: z.string(), commitSha: z.string().optional() }).strict()).default([]), - summary: z.string(), - draft: z.boolean().default(QUESTION_MODEL_DEFAULTS.draft ?? false), - questionType: z.enum(questionTypeValues), - motivation: z.string(), - primaryContributor: contributorReference, - about: z.array(z.string()).default([]), - relatedObjectives: z.array(reference('objectives')).default([]), - relatedProposals: z.array(reference('proposals')).default([]), - relatedBooks: z.array(reference('books')).default([]), - })); - - const objectiveSchema = z.preprocess((value) => preprocessAliasedRecord(objectiveFieldAliases, value), z.object({ - title: z.string(), - description: z.string(), - date: z.coerce.date(), - status: withOptionalDefault(z.enum(statusValues), OBJECTIVE_MODEL_DEFAULTS.status), - groupIds: z.array(z.string()).default([]), - summary: z.string(), - draft: z.boolean().default(OBJECTIVE_MODEL_DEFAULTS.draft ?? false), - timeHorizon: z.enum(timeHorizonValues), - motivation: z.string(), - primaryContributor: contributorReference, - relatedQuestions: z.array(reference('questions')).default([]), - relatedBooks: z.array(reference('books')).default([]), - })); - - const proposalSchema = z.preprocess((value) => preprocessAliasedRecord(proposalFieldAliases, value), z.object({ - title: z.string(), - description: z.string(), - date: z.coerce.date(), - status: withOptionalDefault(z.enum(statusValues), PROPOSAL_MODEL_DEFAULTS.status), - groupIds: z.array(z.string()).default([]), - summary: z.string(), - draft: z.boolean().default(PROPOSAL_MODEL_DEFAULTS.draft ?? false), - proposalType: z.string().min(1).regex(PROPOSAL_TYPE_ID_PATTERN, 'Proposal type must use lowercase kebab-case.'), - motivation: z.string(), - primaryContributor: contributorReference, - relatedObjectives: z.array(reference('objectives')).default([]), - relatedQuestions: z.array(reference('questions')).default([]), - relatedNotes: z.array(reference('notes')).default([]), - relatedBooks: z.array(reference('books')).default([]), - decision: reference('decisions').optional(), - supersedes: z.array(reference('proposals')).default([]), - evidenceRefs: z.array(z.string()).default([]), - plan: z.object({ - desiredOutcome: z.string(), currentProblem: z.string(), proposedApproach: z.string(), - scope: z.array(z.string()), nonGoals: z.array(z.string()), deliverables: z.array(z.string()), - acceptanceCriteria: z.array(z.string()), risks: z.array(z.string()), dependencies: z.array(z.string()), - alternatives: z.array(z.string()), verification: z.array(z.string()), openQuestions: z.array(z.string()).default([]), - }).optional(), - contentProvenance: z.object({ contentPath: z.string(), commitSha: z.string(), digest: z.string(), repositoryId: z.string().optional() }).optional(), - decisionDependencies: z.array(z.object({ projectId: z.string(), decisionId: z.string() }).strict()).default([]), - governanceId: z.string().optional(), - governanceProviderId: z.string().optional(), - governancePolicyId: z.string().optional(), - governanceStatus: z.enum(governanceStatusValues).optional(), - proposalVersion: z.number().int().positive().optional(), - proposalContentHash: z.string().optional(), - votingStartsAt: z.coerce.date().optional(), - votingEndsAt: z.coerce.date().optional(), - canonicalRoute: z.string().optional(), - })); - - const decisionSchema = z.preprocess((value) => preprocessAliasedRecord(decisionFieldAliases, value), z.object({ - title: z.string(), - description: z.string(), - date: z.coerce.date(), - status: withOptionalDefault(z.enum(statusValues), DECISION_MODEL_DEFAULTS.status), - groupIds: z.array(z.string()).default([]), - groupSnapshot: z.object({ - proposalId: z.string(), proposalCommitSha: z.string(), capturedAt: z.coerce.date(), - directGroupIds: z.array(z.string()), effectiveGroupIds: z.array(z.string()), - provenance: z.array(z.object({ groupId: z.string(), kind: z.literal('proposal-snapshot'), viaGroupIds: z.array(z.string()), sourceEntityRefs: z.array(z.string()) }).strict()), - }).strict().optional(), - summary: z.string(), - draft: z.boolean().default(DECISION_MODEL_DEFAULTS.draft ?? false), - decisionType: z.enum(decisionTypeValues), - rationale: z.string(), - authority: z.string(), - primaryContributor: contributorReference, - relatedObjectives: z.array(reference('objectives')).default([]), - relatedQuestions: z.array(reference('questions')).default([]), - relatedNotes: z.array(reference('notes')).default([]), - relatedProposals: z.array(reference('proposals')).default([]), - relatedBooks: z.array(reference('books')).default([]), - supersedes: z.array(reference('decisions')).default([]), - implements: z.array(z.string()).default([]), - immutable: z.boolean().default(false), - governanceDecisionId: z.string().optional(), - governanceProviderId: z.string().optional(), - sourceProposalGovernanceId: z.string().optional(), - sourceProposalVersion: z.number().int().positive().optional(), - sourceProposalHash: z.string().optional(), - governanceRule: z.record(z.unknown()).optional(), - electorateSnapshot: z.record(z.unknown()).optional(), - voteResult: z.record(z.unknown()).optional(), - voterReasons: z.array(z.record(z.unknown())).default([]), - decidedAt: z.coerce.date().optional(), - decisionSnapshotHash: z.string().optional(), - canonicalRoute: z.string().optional(), - })); - + title: z.string(), + description: z.string(), + slug: z.string(), + pageLayout: withOptionalDefault(z.enum(pageLayoutValues), PAGE_MODEL_DEFAULTS.pageLayout), + status: withOptionalDefault(z.enum(statusValues), PAGE_MODEL_DEFAULTS.status), + stage: withOptionalDefault(z.string(), PAGE_MODEL_DEFAULTS.stage), + audience: z.array(z.string()).default(PAGE_MODEL_DEFAULTS.audience ?? []), + summary: z.string(), + updated: z.coerce.date(), + seoTitle: z.string().optional(), + seoDescription: z.string().optional(), + })); + const governed = (model: 'note' | 'question' | 'objective' | 'proposal' | 'decision') => + withPortableContentValidation(model, z.any()); return { pageSchema: withPortableContentValidation('page', pageSchema), - noteSchema: withPortableContentValidation('note', noteSchema), - questionSchema: withPortableContentValidation('question', questionSchema), - objectiveSchema: withPortableContentValidation('objective', objectiveSchema), - proposalSchema: withPortableContentValidation('proposal', proposalSchema), - decisionSchema: withPortableContentValidation('decision', decisionSchema), + noteSchema: governed('note'), + questionSchema: governed('question'), + objectiveSchema: governed('objective'), + proposalSchema: governed('proposal'), + decisionSchema: governed('decision'), }; } diff --git a/tests/unit/content/governance-content-schemas.test.ts b/tests/unit/content/governance-content-schemas.test.ts index 3cfa3b4..281d199 100644 --- a/tests/unit/content/governance-content-schemas.test.ts +++ b/tests/unit/content/governance-content-schemas.test.ts @@ -3,42 +3,32 @@ import { describe, expect, it, vi } from 'vitest'; vi.mock('astro:content', () => ({ reference: () => z.string() })); -function proposal(proposalType: string) { - return { - title: 'Portable proposal type', - description: 'Validates a project-defined proposal type identifier.', - date: '2026-08-12', - status: 'planned', - summary: 'Project proposal types remain portable across SDK and Astro validation.', - proposalType, - motivation: 'Prevent hard-coded proposal classifications from drifting.', - primaryContributor: 'self-hosting-architect', - }; -} +const exactRef = { + store: 'treedx', model: 'objective', id: 'sdk-objective', repository: 'treeseed-ai/sdk-library', + commit: 'a'.repeat(40), path: 'objectives/sdk-objective.yaml', revision: 1, digest: `sha256:${'b'.repeat(64)}`, +}; describe('governance content schemas', () => { - it('accepts portable kebab-case proposal types and rejects malformed identifiers', async () => { + it('uses the canonical SDK proposal contract without a Core-owned compatibility shape', async () => { const { createGovernanceCollectionSchemas } = await import('../../../src/content/governance-schemas.ts'); const { proposalSchema } = createGovernanceCollectionSchemas(); - expect(proposalSchema.safeParse(proposal('customer-defined-review')).success).toBe(true); - const invalid = proposalSchema.safeParse(proposal('Customer Defined Review')); - expect(invalid.success).toBe(false); - if (!invalid.success) expect(invalid.error.issues).toContainEqual(expect.objectContaining({ - path: ['proposalType'], - message: 'Proposal type must use lowercase kebab-case.', - })); + const proposal = { + schemaVersion: 'treeseed.proposal/v1', id: 'portable-proposal', projectId: 'sdk', + title: 'Portable proposal', request: 'Use one governed proposal contract.', status: 'draft', objectiveRefs: [exactRef], + }; + expect(proposalSchema.safeParse(proposal).success).toBe(true); + expect(proposalSchema.safeParse({ ...proposal, proposalType: 'retired-core-shape' }).success).toBe(false); }); - it('runs the SDK portable contract before Astro reference and default adaptation', async () => { + it('reports canonical SDK field diagnostics before Astro consumes content', async () => { const { createGovernanceCollectionSchemas } = await import('../../../src/content/governance-schemas.ts'); const invalid = createGovernanceCollectionSchemas().questionSchema.safeParse({ - title: 'Portable admission', description: 'Core strings alone would accept an empty motivation.', date: '2026-08-12', - status: 'planned', summary: 'SDK validation runs before Astro transformations.', questionType: 'implementation', - motivation: '', primaryContributor: 'self-hosting-architect', + schemaVersion: 'treeseed.question/v1', id: 'portable-question', projectId: 'sdk', subjectRef: exactRef, + question: '', status: 'open', askedAt: '2026-09-21T04:00:00.000Z', }); expect(invalid.success).toBe(false); if (!invalid.success) expect(invalid.error.issues).toContainEqual(expect.objectContaining({ - path: ['motivation'], params: expect.objectContaining({ code: 'content_zod_too_small', model: 'question' }), + path: ['question'], params: expect.objectContaining({ code: 'content_zod_too_small', model: 'question' }), })); }); }); From 2db83bc9914fbb9f247785d8a4caf5c15b3d3fbf Mon Sep 17 00:00:00 2001 From: Adrian Webb Date: Mon, 21 Sep 2026 00:40:25 -0400 Subject: [PATCH 4/6] Adopt TreeDX-only governed fixtures --- .fixtures/treeseed-fixtures | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.fixtures/treeseed-fixtures b/.fixtures/treeseed-fixtures index 7413133..7471ab6 160000 --- a/.fixtures/treeseed-fixtures +++ b/.fixtures/treeseed-fixtures @@ -1 +1 @@ -Subproject commit 74131336841325f417003bb9dd8c4b23e1b478d6 +Subproject commit 7471ab69ac1a0ed5fde1b19321bcfd00d0ca1c00 From 186bf29781db34c59409a808b1a0b6f8d19a2d3b Mon Sep 17 00:00:00 2001 From: Adrian Webb Date: Mon, 21 Sep 2026 00:45:46 -0400 Subject: [PATCH 5/6] Remove duplicate fixture agent authority --- .fixtures/treeseed-fixtures | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.fixtures/treeseed-fixtures b/.fixtures/treeseed-fixtures index 7471ab6..9c0183b 160000 --- a/.fixtures/treeseed-fixtures +++ b/.fixtures/treeseed-fixtures @@ -1 +1 @@ -Subproject commit 7471ab69ac1a0ed5fde1b19321bcfd00d0ca1c00 +Subproject commit 9c0183b1d1ef51179f6ae6da869711cd170cad81 From 9cacf06d9c8e75f8cef1b86cd0ebc25134eacb8a Mon Sep 17 00:00:00 2001 From: Adrian Webb Date: Mon, 21 Sep 2026 00:47:20 -0400 Subject: [PATCH 6/6] Verify TreeDX-only agent authority --- tests/unit/content/group-content-schemas.test.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/unit/content/group-content-schemas.test.ts b/tests/unit/content/group-content-schemas.test.ts index 045e0e6..b5422ab 100644 --- a/tests/unit/content/group-content-schemas.test.ts +++ b/tests/unit/content/group-content-schemas.test.ts @@ -1,4 +1,4 @@ -import { readFileSync } from 'node:fs'; +import { existsSync, readFileSync, readdirSync } from 'node:fs'; import { resolve } from 'node:path'; import { parse } from 'yaml'; import { describe, expect, it } from 'vitest'; @@ -12,12 +12,11 @@ function frontmatter(path: string) { } describe('group content schemas', () => { - it('accepts migrated agents and generated group definitions without legacy tags', () => { + it('keeps group definitions while agent authority remains exclusively in TreeDX', () => { const fixture = resolve('.fixtures/treeseed-fixtures/sites/working-site/src/content'); - const agent = frontmatter(resolve(fixture, 'agents/architect.mdx')); - const group = frontmatter(resolve(fixture, `groups/${agent.groupIds[0]}.md`)); - expect(agent.tags).toBeUndefined(); - expect(agent.groupIds).toEqual(expect.arrayContaining([expect.any(String)])); + const group = frontmatter(resolve(fixture, 'groups/architecture.md')); + const agentDirectory = resolve(fixture, 'agents'); + expect(existsSync(agentDirectory) ? readdirSync(agentDirectory) : []).toEqual([]); expect(createGroupCollectionSchemas().groupSchema.safeParse(group).success).toBe(true); }); });