You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
braid is meant to be a pure framework where the ontology is a plugin (DDD is just the default). But the intent / code worldview is still hard-coded into core, which blocks any non-software ontology (for example a research-notes ontology with roles note / reference) from being a first-class citizen.
This issue tracks decoupling the source-role taxonomy from core so braid is genuinely "braid anything". The bar is OCP: adding a new ontology with its own source roles must require zero edits to @braidhq/core and @braidhq/schema.
Guiding principle
The framework must never branch on a role's identity. It branches only on a capability the ontology declares for that role.
Every leak below is the same violation, asking "are you the thing called intent?" (role === 'intent', mode === 'intent', kind === 'source-intent'). The fix is to ask instead "do you have the capability its sources are enumerated into batch units?". The role name disappears from the framework; the capability stays. The Information Expert for role semantics is the ontology plugin, not core, not the source, not the role name.
Current leaks (verified inventory)
Single root:
packages/schema/src/source.tsSourceRole = z.enum(['code', 'intent']). A closed enum pins the taxonomy. Everything downstream is either this enum flowing through SourceDescriptor.role, or a literal comparison against it.
Framework branches on role identity (these are the OCP violations):
packages/core/src/domain/workspace/Workspace.tscodeSources() / intentSources() filter on the literal role.
packages/core/src/application/BatchService.tsresolveMode (intent vs code vs derive), the mode === 'intent' branch, and buildIntentUnits / IntentLister.
packages/core/src/application/ReactorService.tsisIntentSource gates reactive re-derivation on role === 'intent'; code sources fall through.
packages/schema/src/batch.tsBatchInputMode = z.enum(['intent', 'derive']). The intent value is a framework mode name borrowed from the role.
packages/schema/src/skill.tsSkillInputSourceIntentProvider (kind: 'source-intent') enumerates only role:intent sources for skill forms.
packages/core/src/domain/plugin/OntologyPlugin.tsrequiredSourceRoles is the only role hook an ontology has, and it is typed readonly ('code' | 'intent')[]. Same narrow type is duplicated in packages/sdk/src/defineOntologyPlugin.ts.
Server:
packages/server/src/infrastructure/source/intentScan.ts skips any role !== 'intent' source.
packages/server/src/routes/skills.ts rejects a sourceUnit whose source is not role === 'intent'.
packages/server/src/routes/ontology.ts and packages/schema/src/ontology.tsOntologyResponse expose node and edge types but not roles, so no client can learn an ontology's role vocabulary.
Client:
packages/studio/src/lib/sourceDraft.tsrolePathSegment hard-maps role to the intents / codebases folder.
packages/studio/src/components/AddSourceDialog.tsx and CreateWorkspaceWizard.tsx hard-code the two roles as dropdown options, defaults, and labels.
packages/studio/src/pages/Batch.tsx carries DDD-specific prose ("Reads every registered intent doc", "Will Scan Codebases") that a non-software ontology would render wrong.
Already generic (leave as-is, these are the pattern to follow):
packages/core/src/application/WorkspaceService.tsassertRequiredSourceRoles already diffs presence against a declared list.
packages/schema/src/source.tsLoaderKind is already an open branded string ("new loaders need no edit here"). This is the exact precedent for SourceRole.
packages/core/src/domain/validation/validateEvidence.ts drift surfacing is already generic (it renders driftIssues that upstream skills write). Only the two named flags metadata.intentMissing / implementationMissing (packages/schema/src/model.ts) still bake the worldview in.
Proposed design
1. Open the role type
SourceRole becomes an open branded string, matching the LoaderKind precedent. The intent / code values leave @braidhq/schema and become data the DDD ontology declares.
2. Ontology declares a role vocabulary with capabilities
requiredSourceRoles is replaced by a descriptor list on OntologyPlugin. The capability, not the name, is what the framework reads.
exportinterfaceSourceRoleDescriptor{readonlyid: SourceRolereadonlylabel: string/** Sources of this role must be present for the ontology to run. */readonlyrequired?: boolean/** * Sources of this role are enumerated directly into batch units, * and their sync triggers the Reactor. The name is provisional. */readonlyunitBearing?: boolean/** Workspace subfolder these sources provision into. */readonlypathSegment?: string}// on OntologyPlugin, replacing requiredSourceRolesreadonlysourceRoles: readonlySourceRoleDescriptor[]
required is derived by sourceRoles.filter(r => r.required), so assertRequiredSourceRoles keeps working unchanged. unitBearing is a single capability that drives both batch unit production and reactor gating. They lock onto the same set of sources, so they do not need separate flags. A future divergence can add a second capability without breaking anyone.
3. Framework derives batch behavior from the capability
BatchInputMode stays a closed framework enum (two exhaustive modes) but loses the role name:
Layering: Workspace exposes a pure sourcesWithRole(role: SourceRole), replacing intentSources() / codeSources(). The domain never learns which roles are unit-bearing; the application layer, which already resolves the ontology, does that lookup and calls back into the pure accessor. Domain stays free of the plugin registry.
The same unitBearing capability re-points four call sites: resolveMode, ReactorService.isIntentSource (becomes isUnitBearingSource), intentScan (becomes a generic unitScan injected as unitLister), and the sourceUnit guard in routes/skills.ts.
The DDD extract skill declares role: 'intent' itself. The framework and Studio only know the role parameter.
5. Expose roles to the client
OntologyResponse gains sourceRoles (id, label, capabilities). Studio's Add-Source dropdown, wizard defaults, rolePathSegment, and the Batch-page copy all become driven by the active ontology's declared roles instead of hard-coded literals.
6. SDK
@braidhq/sdk re-exports the SourceRole type, and defineOntologyPlugin takes sourceRoles in place of the narrowly-typed requiredSourceRoles.
Generalize metadata.intentMissing / implementationMissing into a role-agnostic notion of "no evidence yet from role X". This is the cross-role corroboration model that #68 (trust for domains with no privileged fact source) also needs, so the two should be designed together rather than piecemeal.
8. Tests
Framework and @braidhq/test-utils fixtures move to a neutral test ontology (for example roles alpha / beta). Any test that legitimately exercises intent / code moves into packages/ontology-ddd/test.
Acceptance criteria
A third-party ontology can declare its own SourceRole set and run the full extract, propose, HITL, model, view loop without touching @braidhq/core or @braidhq/schema.
ontology-ddd becomes a clean reference: it declarescode / intent as data instead of relying on core knowing those names.
Invariants to keep
The HITL gate and the evidence / provenance rules stay. They are framework invariants, not part of the software-specific taxonomy. Only the source-role taxonomy is being lifted out.
Suggested staging
Open the type and the capability model. Schema role becomes a brand string, SDK gains sourceRoles, core de-hardcodes the four call sites, BatchInputMode renamed. DDD declares its two roles. Framework green.
End-to-end. Server unitScan and the roles endpoint, Studio driven by declared roles, skill-input provider generalized. A non-DDD ontology runs the full loop.
Summary
braid is meant to be a pure framework where the ontology is a plugin (DDD is just the default). But the
intent/codeworldview is still hard-coded into core, which blocks any non-software ontology (for example a research-notes ontology with rolesnote/reference) from being a first-class citizen.This issue tracks decoupling the source-role taxonomy from core so braid is genuinely "braid anything". The bar is OCP: adding a new ontology with its own source roles must require zero edits to
@braidhq/coreand@braidhq/schema.Guiding principle
Every leak below is the same violation, asking "are you the thing called
intent?" (role === 'intent',mode === 'intent',kind === 'source-intent'). The fix is to ask instead "do you have the capability its sources are enumerated into batch units?". The role name disappears from the framework; the capability stays. The Information Expert for role semantics is the ontology plugin, not core, not the source, not the role name.Current leaks (verified inventory)
Single root:
packages/schema/src/source.tsSourceRole = z.enum(['code', 'intent']). A closed enum pins the taxonomy. Everything downstream is either this enum flowing throughSourceDescriptor.role, or a literal comparison against it.Framework branches on role identity (these are the OCP violations):
packages/core/src/domain/workspace/Workspace.tscodeSources()/intentSources()filter on the literal role.packages/core/src/application/BatchService.tsresolveMode(intent vs code vs derive), themode === 'intent'branch, andbuildIntentUnits/IntentLister.packages/core/src/application/ReactorService.tsisIntentSourcegates reactive re-derivation onrole === 'intent';codesources fall through.packages/schema/src/batch.tsBatchInputMode = z.enum(['intent', 'derive']). Theintentvalue is a framework mode name borrowed from the role.packages/schema/src/skill.tsSkillInputSourceIntentProvider(kind: 'source-intent') enumerates onlyrole:intentsources for skill forms.packages/core/src/domain/plugin/OntologyPlugin.tsrequiredSourceRolesis the only role hook an ontology has, and it is typedreadonly ('code' | 'intent')[]. Same narrow type is duplicated inpackages/sdk/src/defineOntologyPlugin.ts.Server:
packages/server/src/infrastructure/source/intentScan.tsskips anyrole !== 'intent'source.packages/server/src/routes/skills.tsrejects asourceUnitwhose source is notrole === 'intent'.packages/server/src/routes/ontology.tsandpackages/schema/src/ontology.tsOntologyResponseexpose node and edge types but not roles, so no client can learn an ontology's role vocabulary.Client:
packages/studio/src/lib/sourceDraft.tsrolePathSegmenthard-maps role to theintents/codebasesfolder.packages/studio/src/components/AddSourceDialog.tsxandCreateWorkspaceWizard.tsxhard-code the two roles as dropdown options, defaults, and labels.packages/studio/src/pages/Batch.tsxcarries DDD-specific prose ("Reads every registered intent doc", "Will Scan Codebases") that a non-software ontology would render wrong.Already generic (leave as-is, these are the pattern to follow):
packages/core/src/application/WorkspaceService.tsassertRequiredSourceRolesalready diffs presence against a declared list.packages/schema/src/source.tsLoaderKindis already an open branded string ("new loaders need no edit here"). This is the exact precedent forSourceRole.packages/core/src/domain/validation/validateEvidence.tsdrift surfacing is already generic (it rendersdriftIssuesthat upstream skills write). Only the two named flagsmetadata.intentMissing/implementationMissing(packages/schema/src/model.ts) still bake the worldview in.Proposed design
1. Open the role type
SourceRolebecomes an open branded string, matching theLoaderKindprecedent. Theintent/codevalues leave@braidhq/schemaand become data the DDD ontology declares.2. Ontology declares a role vocabulary with capabilities
requiredSourceRolesis replaced by a descriptor list onOntologyPlugin. The capability, not the name, is what the framework reads.requiredis derived bysourceRoles.filter(r => r.required), soassertRequiredSourceRoleskeeps working unchanged.unitBearingis a single capability that drives both batch unit production and reactor gating. They lock onto the same set of sources, so they do not need separate flags. A future divergence can add a second capability without breaking anyone.3. Framework derives batch behavior from the capability
BatchInputModestays a closed framework enum (two exhaustive modes) but loses the role name:BatchServicedecides purely from declarations, with no role literal:Layering:
Workspaceexposes a puresourcesWithRole(role: SourceRole), replacingintentSources()/codeSources(). The domain never learns which roles are unit-bearing; the application layer, which already resolves the ontology, does that lookup and calls back into the pure accessor. Domain stays free of the plugin registry.The same
unitBearingcapability re-points four call sites:resolveMode,ReactorService.isIntentSource(becomesisUnitBearingSource),intentScan(becomes a genericunitScaninjected asunitLister), and thesourceUnitguard inroutes/skills.ts.4. Skill-input source provider becomes role-parameterized
The DDD extract skill declares
role: 'intent'itself. The framework and Studio only know theroleparameter.5. Expose roles to the client
OntologyResponsegainssourceRoles(id, label, capabilities). Studio's Add-Source dropdown, wizard defaults,rolePathSegment, and the Batch-page copy all become driven by the active ontology's declared roles instead of hard-coded literals.6. SDK
@braidhq/sdkre-exports theSourceRoletype, anddefineOntologyPlugintakessourceRolesin place of the narrowly-typedrequiredSourceRoles.7. Evidence flags (coordinate with #68)
Generalize
metadata.intentMissing/implementationMissinginto a role-agnostic notion of "no evidence yet from role X". This is the cross-role corroboration model that #68 (trust for domains with no privileged fact source) also needs, so the two should be designed together rather than piecemeal.8. Tests
Framework and
@braidhq/test-utilsfixtures move to a neutral test ontology (for example rolesalpha/beta). Any test that legitimately exercisesintent/codemoves intopackages/ontology-ddd/test.Acceptance criteria
SourceRoleset and run the full extract, propose, HITL, model, view loop without touching@braidhq/coreor@braidhq/schema.grep -rn "'intent'\|'code'\|source-intent" packages/*/src packages/*/testhits onlypackages/ontology-ddd/**.ontology-dddbecomes a clean reference: it declarescode/intentas data instead of relying on core knowing those names.Invariants to keep
The HITL gate and the evidence / provenance rules stay. They are framework invariants, not part of the software-specific taxonomy. Only the source-role taxonomy is being lifted out.
Suggested staging
sourceRoles, core de-hardcodes the four call sites,BatchInputModerenamed. DDD declares its two roles. Framework green.unitScanand the roles endpoint, Studio driven by declared roles, skill-input provider generalized. A non-DDD ontology runs the full loop.Related