diff --git a/adm/gdl/dev/schemas/capability-meta.schema.yaml b/adm/gdl/dev/schemas/capability-meta.schema.yaml new file mode 100644 index 0000000..4e64a2d --- /dev/null +++ b/adm/gdl/dev/schemas/capability-meta.schema.yaml @@ -0,0 +1,140 @@ +# Capability Metadata Schema +# Location: adm/gdl/dev/schemas/capability-meta.schema.yaml +# +# Authority: This schema is the single source of truth for valid capability +# metadata fields. All .meta.yaml files in crp/cap/ must conform to this schema. +# +# DSL Semantics: +# - type: scalar type (string, integer) or structural type (array, object). +# - required: whether the field must be present in every .meta.yaml file. +# - items: element schema for array types. +# - properties: exhaustive property declarations for object types. +# Objects must contain ONLY the declared properties — no additional +# properties are valid. (Resolves AC-09 "exactly" constraint.) +# - enum: closed set of allowed values. +# - format: semantic format constraint (e.g., "date" = ISO 8601 YYYY-MM-DD). +# - description: human-readable purpose for governance readability. +# +# Note (CA-1): The `distinguish_from` field currently exists in ~15 non-empty +# entries across 3 formats in the corpus. Only the object-array format declared +# here is the canonical target format. Migration risk for S2/S3 is Medium. +# +# Traceability (CA-3): The internal structure of `sources` entries (5 required +# properties) is an architecture decision (U2), not an explicit AC requirement. + +schema: + version: "1.0.0" + +fields: + id: + type: string + required: true + description: > + Unique capability identifier. Must match the filename stem of the + corresponding .md file. + + version: + type: string + required: true + description: > + Semantic version (semver) of the capability definition. + + owner: + type: string + required: true + description: > + Semantic Authority over the cognitive contract. Identifies the party + responsible for the meaning, boundaries, and evolution of this capability. + + do_not_use_when: + type: array + required: false + items: + type: string + description: > + Negative applicability boundaries. Each entry describes a situation + where this capability must not be selected. + + distinguish_from: + type: array + required: false + items: + type: object + properties: + id: + type: string + required: true + description: > + Identifier of the capability to distinguish from. + boundary: + type: string + required: true + description: > + Explanation of the semantic boundary between this capability + and the referenced one. + description: > + Neighbor disambiguation entries. Each object identifies a related + capability and the semantic boundary separating them. + + requires: + type: array + required: false + items: + type: string + description: > + Capability IDs that must be co-selected when this capability is active. + + conflicts: + type: array + required: false + items: + type: string + description: > + Capability IDs that must not be co-selected with this capability. + Conflicts must be symmetric (if A conflicts B, B must conflict A). + + # Architecture Decision U2: sources internal structure. + # All 5 properties are required per entry. This is an architecture decision + # (not an explicit AC requirement). The sources array itself is optional — + # authors who do not have full citations omit the field entirely. + sources: + type: array + required: false + items: + type: object + properties: + title: + type: string + required: true + description: > + Title of the reference material. + organization: + type: string + required: true + description: > + Publishing organization or authoring body. + url: + type: string + required: true + description: > + Location where the source can be accessed. + kind: + type: string + required: true + enum: + - standard + - architecture-guidance + - security-guidance + - adoption-guidance + - domain-reference + description: > + Classification of the source material type. + accessed_at: + type: string + required: true + format: date + description: > + Date when the source was last accessed or verified. + description: > + Authoritative sources underpinning the capability's cognitive contract. + Each entry must be fully specified for citation traceability. diff --git a/adm/pbl/ai4x-context-budget-model.md b/adm/pbl/ai4x-context-budget-model.md index 81d71ba..f905107 100644 --- a/adm/pbl/ai4x-context-budget-model.md +++ b/adm/pbl/ai4x-context-budget-model.md @@ -1,5 +1,7 @@ # Context-Budget-Modell fuer Capability-Komposition +> **Status:** Partially absorbed into Epic #37 (Story #72: `token_estimate` field). Budget ceiling and tiered truncation strategy remain parked — depend on `curate` pipeline being conceptually defined. + ## Motivation Wenn `curate` Capabilities aus dem Corpus (`crp/cap/`) zu einem CCC komponiert, kann das resultierende Instruktionsset das Context-Fenster des Ziel-Agent-Hosts sprengen. Aktuell gibt es kein Modell, das dieses Risiko adressiert: diff --git a/adm/pbl/ai4x-lifecycle-feedback-loop.md b/adm/pbl/ai4x-lifecycle-feedback-loop.md index d7b9e1e..d331477 100644 --- a/adm/pbl/ai4x-lifecycle-feedback-loop.md +++ b/adm/pbl/ai4x-lifecycle-feedback-loop.md @@ -1,5 +1,7 @@ # Lifecycle Feedback Loop: evolve / assess +> **Status:** Parked — depends on `curate` being conceptually defined. Revisit when `curate` design work begins. + ## Motivation Das ai4X Operating Model ist aktuell ein Open-Loop-System: `curate` produziert ein Team, `spawn` materialisiert es, danach endet der Lifecycle. Es gibt keinen designten Mechanismus, um: diff --git a/adm/pbl/ai4x-negative-applicability-population.md b/adm/pbl/ai4x-negative-applicability-population.md deleted file mode 100644 index b78b9a2..0000000 --- a/adm/pbl/ai4x-negative-applicability-population.md +++ /dev/null @@ -1,60 +0,0 @@ -# Negative-Applicability Metadaten: Systematische Population - -## Motivation - -Der Capability-Corpus (`crp/cap/`) hat 70+ Capabilities mit `.meta.yaml` Dateien. Drei Felder dienen der negativen Anwendbarkeit — sie kodifizieren, wann eine Capability NICHT verwendet werden soll: - -| Feld | Zweck | Aktueller Fuellgrad | -|------|-------|---------------------| -| `conflicts` | "Darf NICHT zusammen mit Capability X verwendet werden" | **0%** (alle 70 leer) | -| `do_not_use_when` | "Verwende diese Capability nicht, wenn Bedingung Y gilt" | ~27% | -| `distinguish_from` | "Verwechsle diese Capability nicht mit Capability Z" | ~27% | - -## Problem - -Der `requires`-Graph (positive Anwendbarkeit) ist gut befuellt und ermoeglicht Vorwaerts-Komposition: "Was braucht diese Capability?" - -Aber `curate` kann NICHT pruefen, ob zwei Capabilities semantisch inkompatibel sind, weil `conflicts` nirgends befuellt ist. Das Ergebnis: `curate` produziert formal gueltige (alle `requires` erfuellt) aber potenziell semantisch inkohärente Capability-Sets. - -### Konkretes Beispiel - -Zwei hypothetische Capabilities: -- **Capability A**: "Always provide multiple alternatives before deciding" -- **Capability B**: "Make decisive, single-option recommendations without hedging" - -Beide sind einzeln sinnvoll. Zusammen widersprechen sie sich. `conflicts: [capability-b]` in A (und umgekehrt) wuerde `curate` erlauben, diesen Widerspruch zu erkennen und den Nutzer zu warnen oder eine Aufloesung zu verlangen. - -Analoges gilt fuer `do_not_use_when` (kontextabhaengige Ausschlussregeln) und `distinguish_from` (semantische Abgrenzung bei aehnlich klingenden Capabilities). - -## Loesungsrichtung - -### Phase 1: Systematische Analyse - -Alle 70+ Capabilities paarweise auf semantische Inkompatibilitaet pruefen. Ergebnis: eine Conflicts-Matrix. - -Vorgeschlagenes Vorgehen: -1. Pro Domain (Foundation, AI, Analysis, Architecture, Engineering, Strategy) interne Konflikte identifizieren -2. Cross-Domain-Konflikte identifizieren (z.B. Analysis vs. Engineering Reasoning-Stile) -3. `do_not_use_when` fuer alle Capabilities mit kontextabhaengiger Anwendbarkeit befuellen -4. `distinguish_from` fuer alle Capabilities mit semantischer Naehe befuellen - -### Phase 2: Population - -Befuellen der `.meta.yaml` Dateien — idealerweise als Big-Bang in einem Schritt (analog zu Epic #37 AC-Migration). - -### Phase 3: Validierung - -CI-Check erweitern: `conflicts`-Symmetrie pruefen (wenn A conflicts B, muss B conflicts A deklarieren). - -## Bezug zu bestehenden Artefakten - -- **Epic #37**: Definiert `conflicts`, `do_not_use_when`, `distinguish_from` als Schema-Felder — dieses PBL-Item befuellt sie -- **`crp/gov/qlt/capability-authoring-governance.md`**: Authoring-Regeln — muss Population-Anforderungen aufnehmen -- **`utl/cap/checks/capability-meta.mjs`**: Validierung — Symmetrie-Check fuer `conflicts` -- **`adm/pbl/ai4x-context-budget-model.md`**: Context Budget — `conflicts` verhindert auch redundante Capability-Paare, die Budget verschwenden - -## Offene Fragen - -1. Soll die paarweise Analyse manuell (PO/Capability-Governance) oder LLM-gestuetzt erfolgen? -2. Ab welchem Fuellgrad gilt `conflicts` als "ausreichend"? 100% geprueft (auch wenn viele leer bleiben weil kein Konflikt existiert)? -3. Sollen `conflicts` nur harte Inkompatibilitaeten kodifizieren, oder auch weiche Spannungen ("tension" vs. "conflict")? diff --git a/adm/pbl/ai4x-organization-design.md b/adm/pbl/ai4x-organization-design.md index 10fb631..2824f86 100644 --- a/adm/pbl/ai4x-organization-design.md +++ b/adm/pbl/ai4x-organization-design.md @@ -1,5 +1,7 @@ # Organization Design: Collaboration Topology Selection Capability +> **Status:** Parked — depends on `curate` pipeline being conceptually defined. Revisit when `curate` design work begins. + ## Motivation `curate` must perform a critical step that no current team member can execute: diff --git a/utl/cap/checks/tst/capability-meta-schema.test.mjs b/utl/cap/checks/tst/capability-meta-schema.test.mjs new file mode 100644 index 0000000..adb7306 --- /dev/null +++ b/utl/cap/checks/tst/capability-meta-schema.test.mjs @@ -0,0 +1,248 @@ +/** + * Tests for Story #38: Central capability metadata schema definition. + * + * Verifies the structural correctness of + * adm/gdl/dev/schemas/capability-meta.schema.yaml against acceptance criteria + * AC-01 through AC-09 (schema definition scope only — no validation behavior). + * + * The schema uses a Custom YAML DSL (architecture decision U1) with: + * - schema.version for schema versioning + * - fields: as field container (not properties:) + * - per-field required: true/false (not a top-level required array) + * + * Run: node --test utl/cap/checks/tst/capability-meta-schema.test.mjs + */ + +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import YAML from 'yaml'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const SCHEMA_PATH = path.resolve( + __dirname, + '../../../../adm/gdl/dev/schemas/capability-meta.schema.yaml', +); + +function loadSchema() { + const content = fs.readFileSync(SCHEMA_PATH, 'utf8'); + return YAML.parse(content); +} + +describe('capability-meta.schema.yaml — AC-01: existence and parseability', () => { + it('schema file exists at the canonical path', () => { + assert.ok( + fs.existsSync(SCHEMA_PATH), + `Schema file not found at ${SCHEMA_PATH}`, + ); + }); + + it('schema file is parseable YAML', () => { + const schema = loadSchema(); + assert.equal(typeof schema, 'object'); + assert.notEqual(schema, null); + }); + + it('schema declares a version under schema.version', () => { + const schema = loadSchema(); + assert.ok(schema.schema, 'schema block must exist'); + assert.equal(typeof schema.schema.version, 'string'); + assert.ok(schema.schema.version.length > 0, 'schema.version must be non-empty'); + }); + + it('schema uses fields: as field container', () => { + const schema = loadSchema(); + assert.ok(schema.fields, 'fields block must exist'); + assert.equal(typeof schema.fields, 'object'); + }); +}); + +describe('capability-meta.schema.yaml — AC-02: required fields', () => { + it('exactly three fields are marked required: true', () => { + const schema = loadSchema(); + const requiredFields = Object.entries(schema.fields) + .filter(([, def]) => def.required === true) + .map(([name]) => name); + assert.equal(requiredFields.length, 3); + }); + + it('required fields are exactly [id, version, owner]', () => { + const schema = loadSchema(); + const requiredFields = Object.entries(schema.fields) + .filter(([, def]) => def.required === true) + .map(([name]) => name); + const expected = ['id', 'version', 'owner']; + assert.deepEqual(requiredFields.sort(), [...expected].sort()); + }); +}); + +describe('capability-meta.schema.yaml — AC-03: optional fields', () => { + it('fields contains exactly 8 field declarations (3 required + 5 optional)', () => { + const schema = loadSchema(); + const allKeys = Object.keys(schema.fields); + assert.equal(allKeys.length, 8); + }); + + it('optional fields are exactly [do_not_use_when, distinguish_from, requires, conflicts, sources]', () => { + const schema = loadSchema(); + const optionalFields = Object.entries(schema.fields) + .filter(([, def]) => def.required === false) + .map(([name]) => name); + const expected = [ + 'do_not_use_when', + 'distinguish_from', + 'requires', + 'conflicts', + 'sources', + ]; + assert.deepEqual(optionalFields.sort(), [...expected].sort()); + }); +}); + +describe('capability-meta.schema.yaml — AC-05: excluded fields', () => { + const EXCLUDED = [ + 'approved_by', + 'approved_at', + 'scope', + 'status', + 'review_due', + 'migration_note', + ]; + + for (const field of EXCLUDED) { + it(`field "${field}" is NOT declared in schema fields`, () => { + const schema = loadSchema(); + assert.ok( + !(field in schema.fields), + `Excluded field "${field}" must not be present in schema fields`, + ); + }); + } +}); + +describe('capability-meta.schema.yaml — AC-07: owner semantics', () => { + it('owner description contains "Semantic Authority over the cognitive contract"', () => { + const schema = loadSchema(); + const desc = schema.fields.owner.description; + assert.ok( + desc.includes('Semantic Authority over the cognitive contract'), + `owner description must reference "Semantic Authority over the cognitive contract"`, + ); + }); +}); + +describe('capability-meta.schema.yaml — AC-08: do_not_use_when type', () => { + it('do_not_use_when is typed as array', () => { + const schema = loadSchema(); + assert.equal(schema.fields.do_not_use_when.type, 'array'); + }); + + it('do_not_use_when items are typed as string', () => { + const schema = loadSchema(); + assert.equal(schema.fields.do_not_use_when.items.type, 'string'); + }); +}); + +describe('capability-meta.schema.yaml — AC-09: distinguish_from structure', () => { + it('distinguish_from is typed as array', () => { + const schema = loadSchema(); + assert.equal(schema.fields.distinguish_from.type, 'array'); + }); + + it('distinguish_from items are typed as object', () => { + const schema = loadSchema(); + assert.equal(schema.fields.distinguish_from.items.type, 'object'); + }); + + it('distinguish_from items have exactly {id, boundary} properties', () => { + const schema = loadSchema(); + const keys = Object.keys(schema.fields.distinguish_from.items.properties); + assert.deepEqual(keys.sort(), ['boundary', 'id']); + }); + + it('distinguish_from.items.properties.id is typed as string and required', () => { + const schema = loadSchema(); + const idProp = schema.fields.distinguish_from.items.properties.id; + assert.equal(idProp.type, 'string'); + assert.equal(idProp.required, true); + }); + + it('distinguish_from.items.properties.boundary is typed as string and required', () => { + const schema = loadSchema(); + const boundaryProp = schema.fields.distinguish_from.items.properties.boundary; + assert.equal(boundaryProp.type, 'string'); + assert.equal(boundaryProp.required, true); + }); +}); + +describe('capability-meta.schema.yaml — sources structure (U2)', () => { + it('sources is typed as array', () => { + const schema = loadSchema(); + assert.equal(schema.fields.sources.type, 'array'); + }); + + it('sources items are typed as object', () => { + const schema = loadSchema(); + assert.equal(schema.fields.sources.items.type, 'object'); + }); + + it('sources items have exactly 5 properties', () => { + const schema = loadSchema(); + const keys = Object.keys(schema.fields.sources.items.properties); + assert.equal(keys.length, 5); + }); + + it('sources items declare {title, organization, url, kind, accessed_at}', () => { + const schema = loadSchema(); + const keys = Object.keys(schema.fields.sources.items.properties); + const expected = ['title', 'organization', 'url', 'kind', 'accessed_at']; + assert.deepEqual(keys.sort(), [...expected].sort()); + }); + + it('sources.title is typed as string and required', () => { + const schema = loadSchema(); + const prop = schema.fields.sources.items.properties.title; + assert.equal(prop.type, 'string'); + assert.equal(prop.required, true); + }); + + it('sources.organization is typed as string and required', () => { + const schema = loadSchema(); + const prop = schema.fields.sources.items.properties.organization; + assert.equal(prop.type, 'string'); + assert.equal(prop.required, true); + }); + + it('sources.url is typed as string and required', () => { + const schema = loadSchema(); + const prop = schema.fields.sources.items.properties.url; + assert.equal(prop.type, 'string'); + assert.equal(prop.required, true); + }); + + it('sources.kind is typed as string with enum and required', () => { + const schema = loadSchema(); + const kind = schema.fields.sources.items.properties.kind; + assert.equal(kind.type, 'string'); + assert.equal(kind.required, true); + assert.ok(Array.isArray(kind.enum), 'kind must declare an enum'); + const expectedEnum = [ + 'standard', + 'architecture-guidance', + 'security-guidance', + 'adoption-guidance', + 'domain-reference', + ]; + assert.deepEqual([...kind.enum].sort(), [...expectedEnum].sort()); + }); + + it('sources.accessed_at is typed as string with format: date and required', () => { + const schema = loadSchema(); + const accessed = schema.fields.sources.items.properties.accessed_at; + assert.equal(accessed.type, 'string'); + assert.equal(accessed.required, true); + assert.equal(accessed.format, 'date'); + }); +}); diff --git a/utl/cap/package.json b/utl/cap/package.json index 4705428..8c90946 100644 --- a/utl/cap/package.json +++ b/utl/cap/package.json @@ -11,7 +11,7 @@ "check:agent-neutral": "bash checks/agent-neutral.sh", "check:layout-neutral": "bash checks/layout-neutral.sh", "check": "npm run check:meta && npm run check:shape && npm run check:indexes && npm run check:agent-neutral && npm run check:layout-neutral", - "test": "bash checks/tst/capability-meta.sh && bash checks/tst/capability-shape.sh && bash checks/tst/agent-neutral.sh" + "test": "bash checks/tst/capability-meta.sh && bash checks/tst/capability-shape.sh && bash checks/tst/agent-neutral.sh && node --test checks/tst/capability-meta-schema.test.mjs" }, "dependencies": { "yaml": "^2.8.3"