diff --git a/.claude/agents/architecture-review.md b/.claude/agents/architecture-review.md index f66e62d03..cff47a675 100644 --- a/.claude/agents/architecture-review.md +++ b/.claude/agents/architecture-review.md @@ -3,9 +3,6 @@ name: architecture-review description: Architecture and layer responsibility review with zero tolerance enforcement model: opus color: red -skills: - - development-skills:separation-of-concerns - - development-skills:tactical-ddd --- You will return structured JSON output with a single field: @@ -17,46 +14,43 @@ You love failing things. Every FAIL you write is a violation you just caught bef ## Automated by Role Enforcement -The following are now enforced by the oxlint role-enforcement plugin (runs during `lint`): -- Code placement: roles are constrained to specific locations -- Dependency direction: forbiddenImports and forbiddenDependencies rules -- Layer boundaries: entrypoint cannot import persistence, commands cannot import CLI infra -- Use case contracts: single public method, typed inputs/outputs -- Aggregate approval gates - -Do NOT check these — they produce lint errors if violated. Focus on what role enforcement CANNOT check. +Role enforcement checks configured folder structure, location dependency direction, feature isolation, private `_platform` imports, circular imports, role placement, role dependencies, use-case contracts and aggregate approval gates. Run it first and report its failures rather than manually recreating those checks. ## Instructions -1. The [`development-skills:separation-of-concerns`](https://github.com/NTCoding/claude-skillz/blob/main/separation-of-concerns/SKILL.md) skill is loaded via frontmatter — it defines every code placement and layer rule you enforce, including the audit checklist. Read its audit checklist to identify all rule codes. If the skill is not loaded, fetch it from the URL. - Read `docs/architecture/overview.md` — essential context for understanding the project architecture. - Read `docs/architecture/adr/ADR-002-allowed-folder-structures.md` — allowed folder structures per package type. +1. Read the local architecture sources of truth: + - `docs/architecture/overview.md` — project and package architecture + - `docs/architecture/adr/ADR-002-allowed-folder-structures.md` — location responsibilities and dependency rules + - `.riviere/role-enforcement.config.ts` — executable location, dependency and role rules + - `.riviere/role-definitions/index.md` and the referenced local role definitions + - `project-memory/architecture/README.md` and its indexed approved decisions + - `docs/conventions/review-feedback-checks.md` — consumer-mapping ownership checks learned from prior reviews 2. Skip test files (`.spec.ts`, `.test.ts`) — architecture review applies to production code only. 3. For each production file under review, focus on what role enforcement cannot automate: - **Semantic correctness:** Is the `@riviere-role` annotation actually correct for what the code does? - **Mixed responsibilities:** Does a single file/function mix concerns that should be split? - **Feature envy:** Does a method use another class's data more than its own? - **Missing abstractions:** Should code be split that isn't? (e.g., missing repository concept) -4. For separation-of-concerns audit checklist items that overlap with role enforcement (placement, dependency direction), mark as "Automated — enforced by role-enforcement plugin" and skip manual checking. +4. For local rules that role enforcement checks mechanically, record the role-enforcement result instead of duplicating its analysis manually. 5. Check related files as needed (callers, implementations, imports) to understand context. 6. Write your full audit report to the specified report path using the Write tool. 7. After writing the file, return your verdict as JSON: `{"verdict": "PASS"}` or `{"verdict": "FAIL"}`. ## Enforcement Method -Apply the rules from the loaded separation-of-concerns skill mechanically. Do not interpret, contextualize, or weigh circumstances. The rules define what belongs where — your job is to check whether the code matches. +Apply ADR-002, the role-enforcement configuration, local role definitions, conventions and approved architecture memories mechanically. Do not invent or import rules from elsewhere. -The skill's audit checklist is the single source of truth. Do not paraphrase, soften, or add criteria beyond what it states. +The local files listed above are the sources of truth. If they disagree, fail the review and report the contradiction rather than choosing one silently. **Burden of proof:** Code must satisfy every criterion the skill defines. If it fails any criterion, it fails the rule. There is no "overall it's fine" — each criterion is independently required. -**No judgment calls.** If you find yourself weighing pros and cons, you are doing it wrong. The skill already made the judgment call. Apply it. +**No invented judgment calls.** If the local rules do not settle a case, report the ambiguity for a maintainer decision. When in doubt, FAIL. The burden of proof is on the code to demonstrate it belongs, not on the reviewer to prove it doesn't. Do not suggest "this could be improved" — state the rule code and mark FAIL. -**Fix suggestions must comply with the same rules.** Never suggest moving code into a layer where it would also violate. Use the loaded separation-of-concerns skill to determine the correct destination. +**Fix suggestions must comply with the same local rules.** Never suggest moving code into a location where it would also violate. ## Audit Report @@ -69,8 +63,8 @@ List ONLY failures. If PASS, write "No findings." For each finding, use this exact template: ```plaintext -Rule: [code]: [name from skill audit checklist] -Source: development-skills:separation-of-concerns +Rule: [local rule or role] +Source: [local source file] Code: [reviewed file path]:[line range] Verdict: FAIL Description: [what's wrong] @@ -79,7 +73,7 @@ Fix: [what to do — specific file move or restructure] ### 2. Full Audit Trail — organized by file -**CRITICAL:** The audit trail is organized **per file**, not per rule. For EVERY file in "Files to Review", produce a section with a complete audit table covering every rule code from the skill's audit checklist. +**CRITICAL:** The audit trail is organized **per file**, not per rule. For every file in "Files to Review", produce a section covering each applicable rule from the local sources. For each file: @@ -87,10 +81,10 @@ For each file: | # | Rule | Verdict | Evidence | |---|------|---------|----------| -| [code] | [rule name] | PASS / FAIL / N/A | [brief evidence specific to THIS file] | +| [local source/rule] | [rule name] | PASS / FAIL / N/A | [brief evidence specific to THIS file] | | ... | ... | ... | ... | -Repeat for EVERY file. Every rule code from the skill's audit checklist must appear in EVERY file's table. +Repeat for every file. Include each applicable local rule and explain why non-applicable rules are omitted or marked N/A. Verdicts: - **PASS**: Checked in this file, no violations. State what you checked. @@ -122,9 +116,9 @@ Default: Flag issues. Skip only if IMPOSSIBLE (cannot satisfy convention + requi Before generating your response, verify: - [ ] Findings section lists only failures (or "No findings" if PASS) -- [ ] Audit trail has a section for EVERY file, each with a row for EVERY rule code from the skill's audit checklist +- [ ] Audit trail has a section for every file and every applicable local rule - [ ] Audit summary totals match row counts - [ ] Full report written to the file path specified in "Report Path" - [ ] JSON verdict returned: `{"verdict": "PASS"}` or `{"verdict": "FAIL"}` -REMINDER: This is an AUDIT organized by file. Every file must have its own section. Every rule code must have a row in every file's table. Do not group by rule — group by file. +REMINDER: This is an audit organized by file. Every file must have its own section. Do not group by rule — group by file. diff --git a/.dependency-cruiser.mjs b/.dependency-cruiser.mjs deleted file mode 100644 index 9d4b5549c..000000000 --- a/.dependency-cruiser.mjs +++ /dev/null @@ -1,209 +0,0 @@ -export default { - forbidden: [ - { - name: "root-structure", - severity: "error", - comment: "src/ root must only contain structural folders (features/, entrypoint/, platform/, shell/) and index.ts barrel", - from: { path: "(apps|packages|tools)/(?!riviere-schema/|riviere-extract-config/|riviere-extract-conventions/|riviere-role-enforcement/)[^/]+/src/(?!features/|entrypoint/|platform/|shell/|index\\.ts).+" }, - to: {} - }, - { - name: "platform-structure", - severity: "error", - comment: "platform/ contains only domain/ and infra/", - from: { path: "src/platform/(?!domain/|infra/)[^/]+/.+" }, - to: {} - }, - { - name: "feature-structure", - severity: "error", - comment: "Features contain only entrypoint/, commands/, queries/, domain/, data-access/, adapters/, infra/", - from: { path: "features/[^/]+/(?!entrypoint/|commands/|queries/|domain/|data-access/|adapters/|infra/)[^/]+/.+" }, - to: {} - }, - { - name: "no-nested-commands", - severity: "error", - comment: "commands/ must be flat — no nested folders", - from: { path: "features/[^/]+/commands/[^/]+/.+" }, - to: {} - }, - { - name: "no-nested-queries", - severity: "error", - comment: "queries/ must be flat — no nested folders", - from: { path: "features/[^/]+/queries/[^/]+/.+" }, - to: {} - }, - { - name: "entrypoint-no-domain", - severity: "error", - comment: "Entrypoint must never import from domain/", - from: { path: "features/[^/]+/entrypoint/.+" }, - to: { path: "(features/[^/]+/domain/|platform/domain/).+" } - }, - { - name: "entrypoint-restricted-deps", - severity: "error", - comment: "Entrypoint may only import from own feature layers, shared entrypoint code, and platform/infra/", - from: { path: "features/([^/]+)/entrypoint/.+" }, - to: { - path: "src/(features|entrypoint|platform|shell)/", - pathNot: "(features/$1/(entrypoint|commands|queries|infra)/|entrypoint/_platform/|platform/infra/)" - } - }, - { - name: "entrypoint-no-persistence-infra", - severity: "error", - comment: "Entrypoint must not import from persistence or external-client infrastructure", - from: { path: "features/[^/]+/entrypoint/.+" }, - to: { path: "platform/infra/(persistence|external-clients)/.+" } - }, - { - name: "domain-no-upward-deps", - severity: "error", - comment: "Domain must not import from commands/, queries/, entrypoint/, or shell/", - from: { path: "features/[^/]+/domain/.+" }, - to: { path: "(features/[^/]+/(commands|queries|entrypoint)/|shell/).+" } - }, - { - name: "domain-no-infra", - severity: "error", - comment: "Domain must never import from any infra/", - from: { path: "(features/[^/]+/domain/|platform/domain/).+" }, - to: { path: "(platform/infra|features/[^/]+/infra)/.+" } - }, - { - name: "no-cross-feature-imports", - severity: "error", - comment: "Features must not import from other features", - from: { path: "features/([^/]+)/.+" }, - to: { - path: "features/([^/]+)/.+", - pathNot: "features/$1/.+" - } - }, - { - name: "commands-no-cross-feature", - severity: "error", - comment: "Commands forbidden from other features", - from: { path: "features/([^/]+)/commands/.+" }, - to: { - path: "features/([^/]+)/.+", - pathNot: "features/$1/.+" - } - }, - { - name: "commands-no-entrypoint", - severity: "error", - comment: "Commands must not import from entrypoint/", - from: { path: "features/[^/]+/commands/.+" }, - to: { path: "features/[^/]+/entrypoint/.+" } - }, - { - name: "commands-no-http-infra", - severity: "error", - comment: "Commands must not import from http infrastructure", - from: { path: "features/[^/]+/commands/.+" }, - to: { path: "platform/infra/http/.+" } - }, - { - name: "commands-no-cli-infra", - severity: "error", - comment: "Commands must not import from CLI infrastructure (entrypoint concern)", - from: { path: "features/[^/]+/commands/.+" }, - to: { path: "platform/infra/cli/.+" } - }, - { - name: "commands-no-mappers", - severity: "error", - comment: "Commands must not import from feature mappers", - from: { path: "features/[^/]+/commands/.+" }, - to: { path: "features/[^/]+/infra/mappers/.+" } - }, - { - name: "commands-no-middleware", - severity: "error", - comment: "Commands must not import from feature middleware", - from: { path: "features/[^/]+/commands/.+" }, - to: { path: "features/[^/]+/infra/middleware/.+" } - }, - { - name: "queries-no-commands", - severity: "error", - comment: "Queries must not import from commands/", - from: { path: "features/[^/]+/queries/.+" }, - to: { path: "features/[^/]+/commands/.+" } - }, - { - name: "queries-no-entrypoint", - severity: "error", - comment: "Queries must not import from entrypoint/", - from: { path: "features/[^/]+/queries/.+" }, - to: { path: "features/[^/]+/entrypoint/.+" } - }, - { - name: "queries-no-messaging", - severity: "error", - comment: "Queries must not import from messaging infrastructure", - from: { path: "features/[^/]+/queries/.+" }, - to: { path: "platform/infra/messaging/.+" } - }, - { - name: "queries-no-cli-infra", - severity: "error", - comment: "Queries must not import from CLI infrastructure (entrypoint concern)", - from: { path: "features/[^/]+/queries/.+" }, - to: { path: "platform/infra/cli/.+" } - }, - { - name: "queries-no-mappers", - severity: "error", - comment: "Queries must not import from feature mappers", - from: { path: "features/[^/]+/queries/.+" }, - to: { path: "features/[^/]+/infra/mappers/.+" } - }, - { - name: "queries-no-middleware", - severity: "error", - comment: "Queries must not import from feature middleware", - from: { path: "features/[^/]+/queries/.+" }, - to: { path: "features/[^/]+/infra/middleware/.+" } - }, - { - name: "commands-no-queries", - severity: "error", - comment: "Commands must not import from queries/", - from: { path: "features/[^/]+/commands/.+" }, - to: { path: "features/[^/]+/queries/.+" } - }, - { - name: "shell-no-domain", - severity: "error", - comment: "Shell must not import from domain/", - from: { path: "shell/.+" }, - to: { path: "(features/[^/]+/domain/|platform/domain/).+" } - }, - { - name: "platform-no-features", - severity: "error", - comment: "Platform must not import from features/", - from: { path: "platform/.+" }, - to: { path: "features/.+" } - }, - { - name: "no-circular", - severity: "error", - comment: "No circular dependencies", - from: {}, - to: { circular: true } - } - ], - - options: { - doNotFollow: { path: "node_modules" }, - tsPreCompilationDeps: true, - tsConfig: { fileName: "tsconfig.base.json" }, - exclude: ["dist/", "\\.spec\\.", "\\.test\\.", "\\.d\\.ts$", "__fixtures__"] - } -}; diff --git a/.dependency-cruiser.specs.mjs b/.dependency-cruiser.specs.mjs deleted file mode 100644 index 15002dd82..000000000 --- a/.dependency-cruiser.specs.mjs +++ /dev/null @@ -1,30 +0,0 @@ -export default { - forbidden: [ - { - name: "specs-must-be-colocated", - severity: "error", - comment: "Spec files must live next to their production code, not at src/ root", - from: { - path: "(apps|packages|tools)/(?!riviere-schema/|riviere-extract-config/|riviere-extract-conventions/|riviere-role-enforcement/)[^/]+/src/(?!features/|entrypoint/|platform/|shell/|domain/|queries/).+", - pathNot: ["main\\.tsx$", "index\\.ts$", "test/", "test-assertions\\.ts$"] - }, - to: {} - }, - { - name: "feature-structure", - severity: "error", - comment: "Feature files must live in structural subdirs (entrypoint/, commands/, queries/, domain/, components/, hooks/), not at feature root", - from: { - path: "features/[^/]+/(?!entrypoint/|commands/|queries/|domain/|infra/|components/|hooks/)[^/]+$" - }, - to: {} - } - ], - - options: { - doNotFollow: { path: "node_modules" }, - tsPreCompilationDeps: true, - tsConfig: { fileName: "tsconfig.base.json" }, - exclude: ["dist/", "\\.d\\.ts$", "__fixtures__"] - } -}; diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1084d60cc..d7392b61e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -177,7 +177,7 @@ jobs: if ((${#script_files[@]})); then shellcheck "${script_files[@]}" || status=$? fi - shellcheck --shell=sh .husky/commit-msg .husky/pre-commit || status=$? + shellcheck --shell=sh .husky/use-repository-node .husky/commit-msg .husky/pre-commit || status=$? exit "$status" publish: diff --git a/.husky/commit-msg b/.husky/commit-msg index da9948310..f10c549b1 100644 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1 +1,4 @@ -npx --no -- commitlint --edit "$1" +# shellcheck source=.husky/use-repository-node +. "$(dirname "$0")/use-repository-node" + +pnpm exec commitlint --edit "$1" diff --git a/.husky/pre-commit b/.husky/pre-commit index f842b9437..bab950afb 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1,4 @@ -npx lint-staged && pnpm run verify +# shellcheck source=.husky/use-repository-node +. "$(dirname "$0")/use-repository-node" + +pnpm exec lint-staged && pnpm run verify diff --git a/.husky/use-repository-node b/.husky/use-repository-node new file mode 100644 index 000000000..6fdf772dc --- /dev/null +++ b/.husky/use-repository-node @@ -0,0 +1,24 @@ +repository_root="$(git rev-parse --show-toplevel)" +repository_node_directory="${NVM_DIR:-$HOME/.nvm}" + +if [ ! -s "$repository_node_directory/nvm.sh" ]; then + echo "NVM is required to run repository Git hooks." >&2 + exit 1 +fi + +export NVM_DIR="$repository_node_directory" +# shellcheck source=/dev/null +. "$NVM_DIR/nvm.sh" + +hook_starting_directory="$PWD" +cd "$repository_root" || exit 1 +nvm use --silent +node_selection_status=$? +cd "$hook_starting_directory" || exit 1 + +if [ "$node_selection_status" -ne 0 ]; then + exit "$node_selection_status" +fi + +unset FORCE_COLOR NO_COLOR +unset hook_starting_directory node_selection_status repository_node_directory repository_root diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000..60ade1ae0 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24.19.0 diff --git a/.riviere/configurations/app.ts b/.riviere/configurations/app.ts new file mode 100644 index 000000000..ecd9110c8 --- /dev/null +++ b/.riviere/configurations/app.ts @@ -0,0 +1,58 @@ +import { + location, + locationConfiguration, +} from '@living-architecture/riviere-role-enforcement-domain-model' +import type { RoleName } from '../roles' + +const entrypointRoles: RoleName[] = [ + 'cli-entrypoint', + 'cli-output-formatter', + 'command-input-factory', + 'entrypoint-cli-input-parser', +] +const entrypointPlatformCliRoles: RoleName[] = [ + 'entrypoint-cli-input-parser', + 'cli-output-formatter', +] +const cliPresentationRoles: RoleName[] = [ + 'cli-error', + 'cli-output-formatter', + 'cli-response-formatter', + 'cli-response-writer', +] +const shellRoles: RoleName[] = ['main', 'cli-error-handler'] + +export const app = { + locations: locationConfiguration( + location('/features/{feature}', { + entrypoint: { + '{entrypoint}': entrypointRoles, + _platform: { + cli: entrypointPlatformCliRoles, + importRules: { importableFrom: 'withinParentLocation' }, + }, + }, + // Features cannot import from each other. They can only import root infra and commands or queries from any subdomain. + importRules: { + allow: { + root: ['infra'], + anySubdomain: ['commands', 'queries'], + }, + }, + }), + + location('/infra', { + 'cli/presentation': cliPresentationRoles, + importRules: { allow: {} }, + }), + + location('/shell', shellRoles, { + importRules: { + allow: { + root: ['features', 'infra'], + anySubdomain: ['commands', 'queries', 'data-access', 'adapters', 'external-clients'], + }, + }, + }), + ), +} diff --git a/.riviere/configurations/domain-model.ts b/.riviere/configurations/domain-model.ts new file mode 100644 index 000000000..08b1bfb76 --- /dev/null +++ b/.riviere/configurations/domain-model.ts @@ -0,0 +1,26 @@ +import { + location, + locationConfiguration, +} from '@living-architecture/riviere-role-enforcement-domain-model' +import type { RoleName } from '../roles' + +const domainRoles: RoleName[] = [ + 'aggregate', + 'value-object', + 'domain-event', + 'domain-port', + 'domain-service', + 'domain-error', +] + +// A domain model cannot import another domain model or any app or use-case layer. +export const domainModel = { + locations: locationConfiguration( + location('/domain', domainRoles, { + allowAnySubLocations: true, + importRules: { + allow: { anySubdomain: ['published-language'] }, + }, + }), + ), +} diff --git a/.riviere/configurations/published-language.ts b/.riviere/configurations/published-language.ts new file mode 100644 index 000000000..a06d1bc50 --- /dev/null +++ b/.riviere/configurations/published-language.ts @@ -0,0 +1,24 @@ +import { + location, + locationConfiguration, +} from '@living-architecture/riviere-role-enforcement-domain-model' +import type { RoleName } from '../roles' + +const publishedLanguageRoles: RoleName[] = [ + 'published-language-annotation', + 'published-language-schema', + 'published-language-data-structure', + 'published-language-union', + 'published-language-parser', + 'published-language-field-name', + 'value-object', +] + +export const publishedLanguage = { + locations: locationConfiguration( + location('/published-language', publishedLanguageRoles, { + 'eslint-plugin': { roleEnforcement: false }, + importRules: { allow: {} }, + }), + ), +} diff --git a/.riviere/configurations/use-cases.ts b/.riviere/configurations/use-cases.ts new file mode 100644 index 000000000..4ccaf2b54 --- /dev/null +++ b/.riviere/configurations/use-cases.ts @@ -0,0 +1,77 @@ +import { + location, + locationConfiguration, +} from '@living-architecture/riviere-role-enforcement-domain-model' +import type { RoleName } from '../roles' + +const commandRoles: RoleName[] = [ + 'command-use-case', + 'command-use-case-input', + 'command-use-case-result', + 'command-use-case-result-value', +] +const queryRoles: RoleName[] = ['query-model-use-case', 'query-model-use-case-input', 'query-model'] +const dataAccessRoles: RoleName[] = [ + 'aggregate-repository', + 'query-model-loader', + 'data-access-error', +] +const adapterRoles: RoleName[] = ['domain-port-adapter'] +const externalClientRoles: RoleName[] = [ + 'external-client-service', + 'external-client-model', + 'external-client-error', +] + +export const useCases = { + locations: locationConfiguration( + location('/features/{feature}', { + commands: { + roles: commandRoles, + importRules: { + allow: { + sibling: ['data-access'], + ownSubdomain: ['domain'], + anySubdomain: ['published-language'], + }, + }, + }, + queries: { + roles: queryRoles, + importRules: { + allow: { + sibling: ['data-access'], + ownSubdomain: ['domain'], + anySubdomain: ['published-language'], + }, + }, + }, + 'data-access/{concept}': { + roles: dataAccessRoles, + importRules: { + allow: { + sibling: [{ queries: ['query-model'] }], + root: ['infra'], + ownSubdomain: [{ domain: ['aggregate', 'value-object'] }], + anySubdomain: ['published-language'], + }, + }, + }, + 'adapters/{adapter}': { + roles: adapterRoles, + importRules: { + allow: { + root: ['infra'], + ownSubdomain: [{ domain: ['domain-port'] }], + }, + }, + }, + importRules: { allow: {} }, + }), + + location('/infra', { + 'external-clients/{client}': externalClientRoles, + importRules: { allow: {} }, + }), + ), +} diff --git a/.riviere/role-definitions/command-use-case-result-value.md b/.riviere/role-definitions/command-use-case-result-value.md new file mode 100644 index 000000000..86987bd83 --- /dev/null +++ b/.riviere/role-definitions/command-use-case-result-value.md @@ -0,0 +1,28 @@ +# command-use-case-result-value + +## Purpose + +An exported data type used as part of a `command-use-case-result` contract. + +## Rules + +- Lives beside the command result that contains it. +- Contains result data or a closed set of result values. +- Contains no command execution, domain behaviour, persistence or presentation logic. +- Is exported only when another file needs to name the type; otherwise it stays inline in the result. + +## Canonical Example + +```typescript +/** @riviere-role command-use-case-result-value */ +export type AddComponentErrorCode = + | 'VALIDATION_ERROR' + | 'GRAPH_NOT_FOUND' + | 'DUPLICATE_COMPONENT' +``` + +## Anti-Patterns + +- Creating a separate exported type when the shape is used only once and can remain inline. +- Putting methods or orchestration behaviour in a result value. +- Using the role for command inputs or the complete command result. diff --git a/.riviere/role-definitions/command-use-case.md b/.riviere/role-definitions/command-use-case.md index f12b70e83..efc768560 100644 --- a/.riviere/role-definitions/command-use-case.md +++ b/.riviere/role-definitions/command-use-case.md @@ -56,4 +56,4 @@ export class ExtractDraftComponents { ## References - [CQRS Pattern](https://martinfowler.com/bliki/CQRS.html) — Commands vs queries separation -- Separation of Concerns Skill Q3: "Orchestrates write operations?" → commands/ +- ADR-002: "Orchestrates write operations?" → commands/ diff --git a/.riviere/role-definitions/data-access-error.md b/.riviere/role-definitions/data-access-error.md new file mode 100644 index 000000000..533ec568c --- /dev/null +++ b/.riviere/role-definitions/data-access-error.md @@ -0,0 +1,28 @@ +# data-access-error + +## Purpose + +An error describing a failure while loading, reconstructing or persisting application state. + +## Rules + +- Lives in `data-access/{concept}/` beside the repository or loader that can return it. +- Describes a data-access failure, not a violated domain invariant. +- Contains no recovery workflow, presentation behaviour or external-client implementation. + +## Canonical Example + +```typescript +/** @riviere-role data-access-error */ +export class GraphNotFoundError extends Error { + constructor(readonly graphPath: string) { + super(`Graph not found: ${graphPath}`) + } +} +``` + +## Anti-Patterns + +- Labelling domain validation failures as data-access errors. +- Throwing a generic data-access error when a typed result can preserve the concrete failure. +- Placing filesystem or database interaction inside the error class. diff --git a/.riviere/role-definitions/domain-event.md b/.riviere/role-definitions/domain-event.md new file mode 100644 index 000000000..bd3ffadc1 --- /dev/null +++ b/.riviere/role-definitions/domain-event.md @@ -0,0 +1,26 @@ +# domain-event + +## Purpose + +A data-only record of something that happened in the domain. + +## Rules + +- Must be a type alias. +- Must be a data structure containing fields only. +- Must not contain methods, callable properties, call signatures or constructor signatures. + +## Canonical Example + +```typescript +/** @riviere-role domain-event */ +export type WorkflowEvent = + | { type: 'session-started'; at: string } + | { type: 'transitioned'; at: string; from: string; to: string } +``` + +## Anti-Patterns + +- A command requesting that something should happen. +- A mutable object with behaviour. +- A generic message envelope with no domain meaning. diff --git a/.riviere/role-definitions/domain-port-adapter.md b/.riviere/role-definitions/domain-port-adapter.md index f3266fff1..142c69909 100644 --- a/.riviere/role-definitions/domain-port-adapter.md +++ b/.riviere/role-definitions/domain-port-adapter.md @@ -6,9 +6,9 @@ A narrow implementation of one domain port using one generic external-client API ## Rules -1. Implements exactly one domain port. +1. Implements a cohesive domain port. 2. Translates the port input into the external-client input. -3. Invokes exactly one external-client API. +3. Invokes the generic external-client API needed for that translation. 4. Translates the external-client result or error into the port result or error. 5. Contains no domain decisions, application orchestration, or direct infrastructure calls. 6. Lives in `adapters/{adapter}/`. @@ -26,7 +26,7 @@ Allowing the domain-port adapter to import those implementation dependencies wou The real Oxlint implementation added in commit [`2474599b`](https://github.com/NTCoding/living-architecture/commit/2474599b591df037d5e3e5d665e171db65f459a0) demonstrates the boundary. ```typescript -// packages/riviere-role-enforcement/src/features/enforcement/adapters/oxlint/ +// packages/riviere-role-enforcement/use-cases/src/features/enforcement/adapters/oxlint/ // oxlint-role-enforcement-runner.ts export function createOxlintRoleEnforcementRunner( oxlintClient: OxlintClient, @@ -53,7 +53,7 @@ export function createOxlintRoleEnforcementRunner( That adapter knows both contracts: `RoleEnforcementRunnerInput` from the domain port and `OxlintConfig` from the generic Oxlint client. It owns their translation and maps `OxlintExecutionError` into the port's failure result. It does not know how Oxlint is installed or executed. -The external mechanics live in `packages/riviere-role-enforcement/src/platform/infra/external-clients/oxlint/oxlint-client.ts`. That file imports `node:child_process`, `node:fs`, `node:path`, and `node:url`; locates the Oxlint binary; writes the temporary configuration; spawns Oxlint; captures its streams and exit status; and removes the temporary file. It accepts only `OxlintConfig` and primitive paths, so it knows nothing about role-enforcement domain types. +The external mechanics live in `packages/riviere-role-enforcement/use-cases/src/infra/external-clients/oxlint/oxlint-client.ts`. That file imports `node:child_process`, `node:fs`, `node:path`, and `node:url`; locates the Oxlint binary; writes the temporary configuration; spawns Oxlint; captures its streams and exit status; and removes the temporary file. It accepts only `OxlintConfig` and primitive paths, so it knows nothing about role-enforcement domain types. Putting the following code in `oxlint-role-enforcement-runner.ts` would be the violation: @@ -85,10 +85,6 @@ export function createOxlintRoleEnforcementRunner( } ``` -The same rule covers third-party packages. `tools/dev-workflow-v2/src/features/workflow/adapters/github/workflow-pull-request-creator.ts` maps the domain-owned `CreateWorkflowPullRequest` contract to `GithubPullRequestCreationInput` and maps `GithubPullRequest` back to the domain result. It does not import `zod` or execute `gh`. Those details stay together in `tools/dev-workflow-v2/src/platform/infra/external-clients/github/create-pull-request.ts`, where `zod` validates the external JSON and the injected `GhRunner` invokes the external CLI. - -The restriction is intentionally structural: a domain-port adapter may import its domain port and one project-controlled external-client API, but no external package directly. This gives RLE a deterministic rule and prevents a new SDK, CLI, or Node implementation from being hidden in an application adapter under a plausible role name. - ## Anti-Patterns - Importing an aggregate or domain service directly. diff --git a/.riviere/role-definitions/entrypoint-cli-input-parser.md b/.riviere/role-definitions/entrypoint-cli-input-parser.md index 65423a020..5a288acd1 100644 --- a/.riviere/role-definitions/entrypoint-cli-input-parser.md +++ b/.riviere/role-definitions/entrypoint-cli-input-parser.md @@ -13,14 +13,14 @@ Parses or validates raw CLI input using the meaning of a specific entrypoint. ## Canonical Example -Sharing does not change this role into `generic-cli-input-parser` and does not move it to infra. It changes only where the parser lives inside the entrypoint layer. Always choose the narrowest entrypoint scope containing every caller. +Sharing does not move this role to infra. It changes only where the parser lives inside the entrypoint layer. Always choose the narrowest entrypoint scope containing every caller. ### Used by one entrypoint Keep the parser beside that entrypoint: ```text -packages/riviere-cli/src/features/builder/entrypoint/link/ +apps/cli/src/features/builder/entrypoint/link/ ├── entrypoint.ts └── link-source-location-options.ts ``` @@ -70,7 +70,7 @@ export function parseLinkSourceLocation( Move the parser to that feature's private entrypoint platform: ```text -packages/riviere-cli/src/features/{feature}/entrypoint/_platform/cli/ +apps/cli/src/features/{feature}/entrypoint/_platform/cli/ ├── input-parsers/ │ └── {parser}.ts └── option-validators/ @@ -80,26 +80,14 @@ packages/riviere-cli/src/features/{feature}/entrypoint/_platform/cli/ For example, Builder's `validateLinkType` is used by the `link` and `link-external` entrypoints, while `validateHttpMethod` is used by the `link-http` entrypoint and its validator. These are Builder CLI option rules, so their common scope is: ```text -packages/riviere-cli/src/features/builder/entrypoint/_platform/cli/option-validators/ +apps/cli/src/features/builder/entrypoint/_platform/cli/option-validators/ ``` -They must not move to `platform/infra/cli` merely because several Builder entrypoints call them. +They must not move to root `infra/cli` merely because several Builder entrypoints call them. -### Shared by entrypoints in multiple features +### Similar parsing in multiple features -Move the parser to the package's private entrypoint platform: - -```text -packages/riviere-cli/src/entrypoint/_platform/cli/ -├── input-parsers/ -│ └── {parser}.ts -└── option-validators/ - └── {validator}.ts -``` - -The real component-type parsing is the example. `isValidComponentType` is called by Builder entrypoints such as `component-checklist`, and by the Query `components` entrypoint. It encodes application values including `UseCase`, `DomainOp`, and `EventHandler`, so it remains `entrypoint-cli-input-parser` in the shared entrypoint layer. Cross-feature reuse does not make those values generic CLI primitives. - -Nothing outside the containing entrypoint layer may import either `_platform`. `_platform` is private reuse within a layer, not a public shared library. +Features remain isolated. Keep small primitive conversions beside their entrypoints rather than creating a shared abstraction to save a few lines. If substantial cross-feature parsing genuinely emerges, review the app boundary before adding a new location. Domain-aware validation never moves into app infra: pass the raw command or query input through the entrypoint and let the use case parse the domain-owned value object. There is no package-root `src/entrypoint/`; every entrypoint belongs to a feature. ### When generic infra is correct @@ -120,13 +108,13 @@ Do not extract a one-use primitive function merely to make the entrypoint file s ## Common Misclassifications -- Primitive conversion without entrypoint meaning is a `generic-cli-input-parser`. +- Primitive conversion used by an entrypoint remains private to that entrypoint until substantial reuse justifies reviewing the boundary. - Reusable domain validation belongs to the domain that owns the rule. -- Reuse across entrypoints changes the `_platform` scope, not the role or layer. +- Reuse within one feature changes the `_platform` scope, not the role or location. +- Similar code in separate features does not by itself justify a shared abstraction. Domain-aware validation belongs in the subdomain use case and domain model. ## Anti-Patterns - Placing this role in an infrastructure layer. -- Labelling entrypoint-specific parsing as `generic-cli-input-parser`. - Duplicating a parser in several entrypoints instead of moving it to their narrowest common entrypoint `_platform`. - Moving a parser to infra solely because several entrypoints use it. diff --git a/.riviere/role-definitions/generic-cli-input-parser.md b/.riviere/role-definitions/generic-cli-input-parser.md deleted file mode 100644 index 153538549..000000000 --- a/.riviere/role-definitions/generic-cli-input-parser.md +++ /dev/null @@ -1,34 +0,0 @@ -# generic-cli-input-parser - -## Purpose - -Provides generic CLI parsing mechanics without entrypoint, use-case, or domain knowledge. - -## Behavioural Contract - -1. Belongs to the generic infrastructure layer. -2. Parses technical primitive CLI values. -3. Does not import entrypoint, use-case, or domain code. -4. Does not coordinate options belonging to a specific entrypoint. - -## Canonical Example - -There is currently no exported `generic-cli-input-parser` implementation in this repository. Do not create one merely to move code out of an entrypoint or to satisfy a file-size limit. The following is the permitted primitive-only API shape, not evidence that the abstraction is needed: - -```typescript -/** @riviere-role generic-cli-input-parser */ -export function parseInteger(raw: string): number | undefined { - // generic primitive conversion -} -``` - -## Common Misclassifications - -- Parsing options for a particular command is an `entrypoint-cli-input-parser`. -- Reusable domain validation belongs to the domain that owns the rule. -- A parser shared by several entrypoints is still an `entrypoint-cli-input-parser` when it coordinates their options or uses application meaning. Put it in the narrowest common `entrypoint/_platform/cli/input-parsers/` or `option-validators/` scope; reuse alone never makes it generic infra. - -## Anti-Patterns - -- Importing entrypoint, use-case, or domain types. -- Encoding the accepted values of an application concept. diff --git a/.riviere/role-definitions/index.md b/.riviere/role-definitions/index.md index 80262a090..cafd179ad 100644 --- a/.riviere/role-definitions/index.md +++ b/.riviere/role-definitions/index.md @@ -4,30 +4,35 @@ These resources inform how roles are classified and where code should live: -- [Separation of Concerns Skill](https://github.com/NTCoding/claude-skillz/blob/main/separation-of-concerns/SKILL.md) — Code placement decision tree (Q1-Q7): wiring → entrypoint → commands → queries → domain → infra -- [Tactical DDD Skill](https://github.com/NTCoding/claude-skillz/blob/main/tactical-ddd/SKILL.md) — Aggregate design, value objects, domain services, repositories - [ADR-002: Allowed Folder Structures](../../docs/architecture/adr/ADR-002-allowed-folder-structures.md) — Canonical directory layout +- [Role enforcement configuration](../role-enforcement.config.ts) — Executable location, dependency, and role rules +- [Architecture memory](../../project-memory/architecture/README.md) — Approved local architecture decisions and examples - [Software Design Conventions](../../docs/conventions/software-design.md) — SD-001 through SD-023 ## Dependency Rules Dependencies point inward: -- `entrypoint/` → commands and queries; never domain or data access directly -- `commands/` → domain and data access; never concrete domain-port adapters -- `queries/` → query models and data access -- `domain/` → domain code and domain ports only; never adapters or infrastructure -- `data-access/` → reconstructs aggregates or query models from persisted data -- `adapters/` → one domain port and one generic client API; never external packages directly -- `infra/` → external packages and generic technical capabilities whose APIs use only language primitives or external-system types; never application-owned code from entrypoint, commands, queries, domain, data access, adapters, or shell -- `shell/` → constructs concrete dependencies and passes them into entrypoints -Concrete test: `readJsonFile(filePath): unknown` and `resolveFileOrPackagePath(...): string` qualify because their contracts contain only primitives and external technical concepts. `loadDraftComponentsFromFile(filePath): DraftComponent[]` does not qualify because its contract and validation use an application-owned type. See the [full extraction repository example](../../project-memory/architecture/memories/prefer-layer-based-rules.md). +- App `entrypoint/` → subdomain commands and queries plus app `infra/`; never domain or data access directly +- Use-case `commands/` → own-subdomain domain and feature data access; never concrete domain-port adapters +- Use-case `queries/` → own-subdomain domain and feature data access +- Domain-model `domain/` → its own model and permitted published languages; never use cases, adapters, infra, apps, or another domain model +- Use-case `data-access/{concept}/` → aggregate and value-object roles from its own domain plus generic clients; never domain services +- Use-case `adapters/{adapter}/` → domain ports from its own subdomain plus generic client APIs; never external packages directly +- Root `infra/` → external packages and generic technical capabilities; never app, use-case, or domain declarations +- App `shell/` → constructs concrete dependencies and passes them into entrypoints + +Concrete test: `readJsonFile(filePath): unknown` and `resolveFileOrPackagePath(...): string` qualify because their contracts contain only primitives and external technical concepts. `loadDraftComponentsFromFile(filePath): DraftComponent[]` does not qualify because its contract and validation use an application-owned type. ## Automated Enforcement -Role enforcement is automated via an oxlint plugin. It checks annotations, location constraints, dependency rules, and I/O contracts at lint time. The enforcement config at `.riviere/role-enforcement.config.ts` is the source of truth for what's enforced. The separation-of-concerns skill defines the architectural principles; role enforcement automates their verification. +Rivière role enforcement is automated via an Oxlint plugin. It checks annotations, location constraints, import rules, and input/output contracts at lint time. ADR-002 defines the architecture and `.riviere/role-enforcement.config.ts` is its executable form. Changes must update both. + +Import rules belong in the relevant location's `importRules`. Imports are unrestricted until a location declares import rules. That location can then import only its own subtree, inherited imports, and locations listed in `allow`. `sibling` means the same concrete parent location; `root` means the same package root; `ownSubdomain` and `anySubdomain` use the configured `{subdomain}` path capture. Allowing a location allows everything inside it unless a role subset is supplied. Explicit sublocations are the complete list of permitted folders unless `allowAnySubLocations` is set. + +The enforcer checks static imports, re-exports, dynamic imports, CommonJS `require()` calls and TypeScript import types. Non-literal dynamic imports and `require()` calls are rejected because their target cannot be checked. Production code cannot import ignored fixture files. Test files are deliberately exempt from production import rules so tests can assemble fixtures across boundaries. -Import rules belong directly to their `location(...)` or `subLocation(...)` definitions. Imports within the same configured location are allowed normally. A location may restrict imports crossing its boundary to specific target roles or forbid direct external-package imports. Role-specific exceptions, such as command-to-command and adapter-to-adapter imports, use the existing role `forbiddenDependencies` rule. RLE must not maintain a second list of path matchers for architectural layers. +Some behavioural guidance is intentionally reviewed rather than mechanically counted. For example, an adapter should stay focused on one port-to-client translation and a CLI entrypoint should invoke one use case. The executable rules enforce the permitted locations and roles; review checks whether a particular declaration remains cohesive. ## Classification Decision Tree diff --git a/.riviere/role-definitions/published-language-annotation.md b/.riviere/role-definitions/published-language-annotation.md new file mode 100644 index 000000000..139c5282f --- /dev/null +++ b/.riviere/role-definitions/published-language-annotation.md @@ -0,0 +1,30 @@ +# published-language-annotation + +## Purpose + +An annotation that forms part of a published language and is applied to source-code declarations. + +## Canonical Example + +```typescript +/** @riviere-role published-language-annotation */ +export function UseCase(target: T, context: ClassDecoratorContext): T { + return target +} +``` + +Annotation factories are also valid: + +```typescript +/** @riviere-role published-language-annotation */ +export function HttpClient( + serviceName: string, +): (target: T, context: ClassDecoratorContext) => T { + return (target) => target +} +``` + +## Anti-Patterns + +- An ordinary exported function is not an annotation. +- A function that reads an annotation is not itself an annotation. diff --git a/.riviere/role-definitions/published-language-data-structure.md b/.riviere/role-definitions/published-language-data-structure.md new file mode 100644 index 000000000..ad95b2d5b --- /dev/null +++ b/.riviere/role-definitions/published-language-data-structure.md @@ -0,0 +1,21 @@ +# published-language-data-structure + +## Purpose + +A method-free data structure used inside a published language schema. + +## Canonical Example + +```typescript +/** @riviere-role published-language-data-structure */ +export interface Link { + source: string + target: string +} +``` + +## Anti-Patterns + +- It cannot contain methods or function-valued fields. +- It is not the complete published schema. +- It is not an application or domain service. diff --git a/.riviere/role-definitions/published-language-field-name.md b/.riviere/role-definitions/published-language-field-name.md new file mode 100644 index 000000000..561716dd0 --- /dev/null +++ b/.riviere/role-definitions/published-language-field-name.md @@ -0,0 +1,18 @@ +# published-language-field-name + +## Purpose + +The exact name of a field defined by a published language, exported so producers and consumers use the same spelling. + +## Canonical Example + +```typescript +/** @riviere-role published-language-field-name */ +export const EVENT_NAME_FIELD = 'eventName' as const +``` + +## Anti-Patterns + +- A mutable variable is not a published field name. +- A computed value is not a published field name. +- An application setting or implementation constant is not part of a published language. diff --git a/.riviere/role-definitions/published-language-parser.md b/.riviere/role-definitions/published-language-parser.md new file mode 100644 index 000000000..341a41870 --- /dev/null +++ b/.riviere/role-definitions/published-language-parser.md @@ -0,0 +1,22 @@ +# published-language-parser + +## Purpose + +Parses input into the complete schema of a published language without throwing validation failures. + +## Canonical Example + +```typescript +/** @riviere-role published-language-parser */ +export function parseRiviereGraph(value: unknown): + | { success: true; graph: RiviereGraph } + | { success: false; issues: ValidationIssue[] } { + // parsing omitted +} +``` + +## Anti-Patterns + +- The success branch must contain the published-language schema. +- A parser must return an explicit failure branch rather than throwing validation failures. +- An arbitrary conversion or formatting function is not a published-language parser. diff --git a/.riviere/role-definitions/published-language-schema.md b/.riviere/role-definitions/published-language-schema.md new file mode 100644 index 000000000..71b829234 --- /dev/null +++ b/.riviere/role-definitions/published-language-schema.md @@ -0,0 +1,22 @@ +# published-language-schema + +## Purpose + +The complete data structure exchanged through a published language. + +## Canonical Example + +```typescript +/** @riviere-role published-language-schema */ +export interface RiviereGraph { + version: string + components: Component[] + links: Link[] +} +``` + +## Anti-Patterns + +- It cannot contain methods or function-valued fields. +- A structure nested inside the complete schema is a `published-language-data-structure`. +- It is not an application or domain model. diff --git a/.riviere/role-definitions/published-language-union.md b/.riviere/role-definitions/published-language-union.md new file mode 100644 index 000000000..4cf6e5335 --- /dev/null +++ b/.riviere/role-definitions/published-language-union.md @@ -0,0 +1,17 @@ +# published-language-union + +## Purpose + +A closed set of alternatives defined by a published language. + +## Canonical Example + +```typescript +/** @riviere-role published-language-union */ +export type LinkType = 'sync' | 'async' +``` + +## Anti-Patterns + +- A type alias that is not a union is not a published-language union. +- It must not contain application-specific alternatives. diff --git a/.riviere/role-definitions/query-model-error.md b/.riviere/role-definitions/query-model-error.md deleted file mode 100644 index c7e4c2feb..000000000 --- a/.riviere/role-definitions/query-model-error.md +++ /dev/null @@ -1,33 +0,0 @@ -# query-model-error - -## Purpose -A custom error class for exceptional conditions in the query model layer. - -## Behavioral Contract -1. Extends `Error` -2. Represents a query-specific error condition (e.g., component not found, invalid query) -3. Lives in the `/queries` layer alongside query-model types -4. Provides a descriptive error message with context for debugging - -## Examples - -### Canonical Example -```typescript -/** @riviere-role query-model-error */ -export class ComponentNotFoundError extends Error { - constructor(componentId: string) { - super(`Component not found: ${componentId}`) - this.name = 'ComponentNotFoundError' - } -} -``` - -## Anti-Patterns - -### Common Misclassifications -- **Not a domain-error**: domain errors live in the `/domain` layer and relate to domain invariant violations. Query model errors relate to query operations. -- **Not a cli-error**: CLI errors handle presentation-layer error formatting. Query model errors are thrown by query logic. - -## Decision Guidance -- **vs domain-error**: Is this error thrown during query model operations? → query-model-error. Is it thrown during domain behavior or aggregate invariant enforcement? → domain-error -- **vs cli-error**: Is this error about query logic? → query-model-error. Is it about CLI presentation? → cli-error diff --git a/.riviere/role-definitions/query-model-loader.md b/.riviere/role-definitions/query-model-loader.md index 1b15d4638..536882256 100644 --- a/.riviere/role-definitions/query-model-loader.md +++ b/.riviere/role-definitions/query-model-loader.md @@ -1,11 +1,11 @@ # query-model-loader ## Purpose -A class that loads a query model from persisted state — the read-only counterpart of an aggregate-repository. +A class that loads a concrete query model for one read use case from persisted state — the read-only counterpart of an aggregate-repository. ## Behavioral Contract -1. **Load** — assemble the query model from persisted state (files, database, APIs) and return it -2. The loader MUST return a query-model, not raw data or partial state +1. **Load** — assemble the query model for a concrete query use case from persisted state (files, database, APIs) and return it +2. The loader MUST return that concrete query model, not raw persisted state or a reusable domain service 3. May use external-client-services internally to access storage or parsers 4. **No save method** — query model loaders are strictly read-only @@ -14,20 +14,26 @@ A class that loads a query model from persisted state — the read-only counterp ### Canonical Example ```typescript /** @riviere-role query-model-loader */ -export class RiviereQueryLoader { - load(graphPathOption?: string): RiviereQuery { - const graphPath = this.resolveGraphPath(graphPathOption) - const content = readFileSync(graphPath, 'utf-8') - const parsed: unknown = JSON.parse(content) - return RiviereQuery.fromJSON(parsed) +export class ComponentListLoader { + load( + graphPath: string | undefined, + domain: string | undefined, + type: ComponentType | undefined, + ): ComponentList { + const components = loadQuery(graphPath).components() + const inDomain = domain === undefined + ? components + : components.filter((component) => component.domain === domain) + + return { + components: type === undefined + ? inDomain + : inDomain.filter((component) => component.type === type), + } } } ``` -### Edge Cases -- A loader may have multiple load methods for different access patterns -- Private helper methods are implementation details, not separate roles - ## Anti-Patterns ### Common Misclassifications @@ -37,10 +43,9 @@ export class RiviereQueryLoader { ### Mixed Responsibility Signals - If the loader has a save/persist method — it may be an aggregate-repository -- If the loader returns raw data instead of a query model — it may be an external-client-service -- If the loader performs business logic after loading — that belongs on the query model +- If domain behaviour is needed to shape the read, the responsibility is outside data access. The loader may import only the query model contract needed for its return value. ## Decision Guidance - **vs aggregate-repository**: Does it save state? → aggregate-repository. Load only, returning a query-model? → query-model-loader -- **vs external-client-service**: Does it return a query-model? → query-model-loader. Does it return raw data? → external-client-service +- **vs external-client-service**: `readJsonFile(path): unknown` is a generic filesystem client operation. `ComponentListLoader.load(...): ComponentList` uses persisted graph data to build the concrete query model. - **vs query-model-use-case**: Does it only load? → query-model-loader. Does it orchestrate load + query + return? → query-model-use-case diff --git a/.riviere/role-definitions/query-model-use-case.md b/.riviere/role-definitions/query-model-use-case.md index 91f32d250..96be52ec3 100644 --- a/.riviere/role-definitions/query-model-use-case.md +++ b/.riviere/role-definitions/query-model-use-case.md @@ -1,13 +1,13 @@ # query-model-use-case ## Purpose -A class that orchestrates a read-only operation: loading a query model and returning computed results without side effects. Dependencies are injected via constructor. +A class that orchestrates one read-only operation by loading and returning its concrete query model without side effects. Dependencies are injected via constructor. ## Behavioral Contract A query model use case class has exactly one public method (`execute`) that follows this sequence: -1. **Load** — use the injected query-model-loader to load the query model from persisted state -2. **Query** — call method(s) on the query model to compute results -3. **Return** — return query-model types directly +1. **Translate** — translate the use-case input into criteria for its query-model-loader +2. **Load** — use the injected loader to build the concrete query model +3. **Return** — return that query model directly No state is modified. No saving occurs. The query model is never mutated. @@ -18,12 +18,11 @@ The `execute` method accepts exactly one parameter typed as a `query-model-use-c ### Canonical Example ```typescript /** @riviere-role query-model-use-case */ -export class ListDomains { - constructor(private readonly repository: RiviereQueryRepository) {} +export class ListComponents { + constructor(private readonly components: ComponentListLoader) {} - execute(input: ListDomainsInput): ListDomainsResult { - const query = this.repository.load(input.graphPathOption) - return { domains: query.domains() } + execute(input: ListComponentsInput): ComponentList { + return this.components.load(input.graphPath, input.domain, input.type) } } ``` @@ -31,12 +30,14 @@ export class ListDomains { ### Edge Cases - A query that calls multiple methods on the same query model is valid - A query that composes results from multiple query model methods is valid +- A use case may map known loader failures into query-use-case errors +- A use case may coordinate multiple loaders when the concrete read genuinely needs them ## Anti-Patterns ### Common Misclassifications - **Not a command-use-case**: commands orchestrate write operations that may modify and save state. If nothing is modified or saved, use query-model-use-case. -- **Not a domain-service**: domain services contain pure business logic. If it loads a query model from persistence, it is a query-model-use-case. +- **Not a domain-service**: domain services contain reusable domain logic. If it coordinates loading a concrete read from persistence, it is a query-model-use-case. - **Not a cli-entrypoint**: entrypoints translate external input into query-model-use-case-input and call the use case. They do not load query models. ### Mixed Responsibility Signals @@ -47,7 +48,7 @@ export class ListDomains { ## Decision Guidance - **vs command-use-case**: Does it modify or save state? → command-use-case. Read-only with no side effects? → query-model-use-case -- **vs domain-service**: Does it load a query model from persistence? → query-model-use-case. Pure logic on passed-in data? → domain-service +- **vs domain-service**: Does it coordinate loading a concrete read from persistence? → query-model-use-case. Reusable domain logic on passed-in data? → domain-service ## References - [CQRS Pattern](https://martinfowler.com/bliki/CQRS.html) — Commands vs queries separation diff --git a/.riviere/role-definitions/query-model.md b/.riviere/role-definitions/query-model.md index 9ab71836c..3ef86c478 100644 --- a/.riviere/role-definitions/query-model.md +++ b/.riviere/role-definitions/query-model.md @@ -1,7 +1,7 @@ # query-model ## Purpose -A class, interface, or type that represents the read-side model — the counterpart of an aggregate on the write side. Includes the query model class itself and the types it returns. +A class, interface, or type shaped for the result of a concrete query use case. Includes the query model class itself and the types it returns. ## Behavioral Contract @@ -17,46 +17,91 @@ Represents a result shape returned by query model methods. These are the types t ## Examples -### Query Model Class +### Design from a concrete query + +Use case: a user runs: + +```text +riviere components --domain payments --type API +``` + +`ListComponents` must return only API components from the `payments` domain. + +The query model for that use case is the component list: + ```typescript /** @riviere-role query-model */ -export class RiviereQuery { - private readonly graph: RiviereGraph +export interface ComponentList { + components: Component[] +} +``` + +The loader builds that model specifically for the requested read: - constructor(graph: RiviereGraph) { - assertValidGraph(graph) - this.graph = graph +```typescript +/** @riviere-role query-model-loader */ +export class ComponentListLoader { + load( + graphPath: string | undefined, + domain: string | undefined, + type: ComponentType | undefined, + ): ComponentList { + const components = loadQuery(graphPath).components() + const inDomain = domain === undefined + ? components + : components.filter((component) => component.domain === domain) + + return { + components: type === undefined + ? inDomain + : inDomain.filter((component) => component.type === type), + } } +} +``` - domains(): Domain[] { - return queryDomains(this.graph) +The use case translates its input into loader criteria and returns the loaded model: + +```typescript +/** @riviere-role query-model-use-case */ +export class ListComponents { + constructor(private readonly components: ComponentListLoader) {} + + execute(input: ListComponentsInput): ComponentList { + return this.components.load(input.graphPath, input.domain, input.type) } } ``` -### Query Model Result Type +Bad: + ```typescript -/** @riviere-role query-model */ -export interface Domain { - name: string - componentCounts: ComponentCounts +export class RiviereQueryRepository { + load(): RiviereQuery } +``` -/** @riviere-role query-model */ -export type DomainSummary = ReturnType[number] +The persisted state is a graph. `RiviereQuery` is domain behaviour used to build the query-specific `ComponentList`; it is not itself the query model. + +Also bad: + +```typescript +export class GraphQueryModel {} ``` +There is no user query called “query graph”. This generic model hides the actual use case and prevents the read from being shaped around what `ListComponents` needs. + ### Edge Cases -- A query model class with many public methods (facade pattern) is valid -- A query model class that delegates to pure functions is the canonical pattern -- Static factory methods (e.g., `fromJSON`) are valid -- Branded types used by the query model (e.g., `ComponentId`) are valid +- Usually there is one query model per query use case because each read can be shaped and optimised independently +- Share a query model only when concrete use cases genuinely need the same read shape +- Query models may import the domain objects and behaviour needed to build that read +- Do not add `Model` or `QueryModel` suffixes; the role annotation already states the technical classification ## Anti-Patterns ### Common Misclassifications - **Not an aggregate**: Aggregates enforce behavioral invariants and expose methods that modify state. If no method modifies state, it is a query-model. -- **Not a domain-service**: Domain services are stateless functions. Query model classes hold state. +- **Not a domain-service**: A domain service provides reusable domain behaviour. A query model is shaped for a concrete read use case. - **Not a value-object**: Value objects are reusable domain concepts in the `/domain` layer. Query model types live in the `/queries` layer. ### Mixed Responsibility Signals @@ -66,7 +111,7 @@ export type DomainSummary = ReturnType[number] ## Decision Guidance - **vs aggregate**: Does any method modify state? → aggregate. All methods read-only? → query-model -- **vs domain-service**: Does it hold state? → query-model. Stateless function operating on passed-in data? → domain-service +- **vs domain-service**: Is it reusable domain behaviour? → domain-service. Is it a read shaped for a concrete query use case? → query-model - **vs value-object**: Does it live in `/queries`? → query-model. Does it live in `/domain`? → value-object ## References diff --git a/.riviere/role-definitions/value-object.md b/.riviere/role-definitions/value-object.md index 36911660b..1612ace85 100644 --- a/.riviere/role-definitions/value-object.md +++ b/.riviere/role-definitions/value-object.md @@ -1,35 +1,47 @@ # value-object ## Purpose -A type or class that represents a domain concept defined by its attributes rather than identity — it carries meaning but no behavior that modifies external state. +A class that represents a domain concept defined by its attributes rather than identity. It owns its parsing and may expose immutable behavior. ## Behavioral Contract 1. Defined by its values, not by an identity -2. Typically immutable -3. May have derived/computed properties but no side effects -4. Used as building blocks within aggregates, inputs, and results +2. Immutable: operations return new values instead of mutating the current value +3. Has at least one static parsing method named `parse` or beginning with `parseFrom` +4. Has a private constructor, so callers must use a parsing method +5. May expose instance methods such as `equals`, `add`, or `toString` +6. Does not store functions in instance data members +7. Used as a building block within aggregates, inputs, and results ## Examples ### Canonical Example ```typescript /** @riviere-role value-object */ -export interface ModuleContext { - moduleName: string - sourceFiles: SourceFile[] - tsConfigPath: string +export class Money { + declare private readonly brand: 'Money' + + private constructor(readonly amount: number) {} + + static parse(amount: number): Money { + return new Money(amount) + } + + add(other: Money): Money { + return Money.parse(this.amount + other.amount) + } } ``` ### Edge Cases -- Discriminated unions are value objects: `type Outcome = 'success' | 'partial' | 'failure'` -- Enum-like const objects can be value objects -- A class with only getters and no mutation methods is a value object +- Multiple input representations may use methods such as `parseFromString` and `parseFromJson` +- Parsing may return a structured validation result when input can be invalid +- Ordinary instance methods are allowed; callable instance data members are not ## Anti-Patterns ### Common Misclassifications - **Not an aggregate**: if it owns behavior that enforces invariants and is loaded through a repository, it's an aggregate +- **Not an interface or type alias**: value objects are classes that own parsing and immutable behavior - **Not a command-use-case-input**: if it's specifically the parameter type for a command, use that more specific role - **Not an external-client-model**: if it represents an external service's data shape rather than a domain concept - **Not a consumer contract**: if it exists only to shape data for a builder, presenter, workflow, or CLI consumer, it is not a domain value object diff --git a/.riviere/role-enforcement.config.ts b/.riviere/role-enforcement.config.ts index 194f6b0c5..598a3e3da 100644 --- a/.riviere/role-enforcement.config.ts +++ b/.riviere/role-enforcement.config.ts @@ -1,109 +1,28 @@ -import { location, roleEnforcement } from '@living-architecture/riviere-role-enforcement' -import { allRoles, type RoleName } from './roles' - -const commandRoles: RoleName[] = [ - 'command-use-case', - 'command-use-case-input', - 'command-use-case-result', - 'command-use-case-result-value', - 'command-input-factory', -] - -const queryRoles: RoleName[] = [ - 'query-model-use-case', - 'query-model-use-case-input', - 'query-model', - 'query-model-error', -] - -const domainRoles: RoleName[] = [ - 'aggregate', - 'value-object', - 'domain-event', - 'domain-port', - 'domain-service', - 'domain-error', -] - -const externalClientRoles: RoleName[] = [ - 'external-client-service', - 'external-client-model', - 'external-client-error', -] - -const entrypointRoles: RoleName[] = [ - 'cli-entrypoint', - 'cli-error-handler', - 'cli-output-formatter', - 'command-input-factory', - 'entrypoint-cli-input-parser', -] - -const cliPresentationRoles: RoleName[] = [ - 'cli-error', - 'cli-error-handler', - 'cli-output-formatter', - 'cli-response-formatter', - 'cli-response-writer', -] - -const packages = [ - 'packages/riviere-cli', - 'packages/riviere-extract-ts', - 'packages/riviere-builder', - 'packages/riviere-query', - 'packages/riviere-role-enforcement', - 'tools/dev-workflow-v2', -] - -export const config = roleEnforcement({ - packages, - canonicalConfigurationsFile: '.riviere/canonical-role-configurations.md', - ignorePatterns: [ - '**/*.spec.ts', - '**/__fixtures__/**', - '**/*-fixtures.ts', - '**/test-fixtures.ts', - '**/test-fixture-*.ts', - ], +import { roleEnforcementConfiguration } from '@living-architecture/riviere-role-enforcement-domain-model' +import { app } from './configurations/app' +import { domainModel } from './configurations/domain-model' +import { publishedLanguage } from './configurations/published-language' +import { useCases } from './configurations/use-cases' +import { allRoles } from './roles' + +/** + * Executable enforcement of: + * docs/architecture/adr/ADR-002-allowed-folder-structures.md + * + * ADR-002 and this configuration must remain aligned. + * Any change to the architecture must update both. + */ + +export const config = roleEnforcementConfiguration({ + configurations: { + 'apps/': app, + 'packages/{subdomain}/domain-model': domainModel, + 'packages/{subdomain}/published-language': publishedLanguage, + 'packages/{subdomain}/use-cases': useCases, + 'tools/': app, + }, + ignorePatterns: ['**/__fixtures__/**'], roleDefinitionsDir: '.riviere/role-definitions', roles: allRoles, - workspacePackageSources: { - '@living-architecture/riviere-builder': 'packages/riviere-builder/src/index.ts', - '@living-architecture/riviere-query': 'packages/riviere-query/src/index.ts', - }, - - locations: [ - location('src/features/{feature}') - .subLocation('/entrypoint/{entrypoint}', entrypointRoles, { - forbiddenImports: ['**/domain/**', '**/data-access/**'], - }) - .subLocation('/commands', commandRoles, { - forbiddenImports: ['**/infra/cli/**'], - }) - .subLocation('/queries', queryRoles, { forbiddenImports: ['**/infra/cli/**'] }) - .subLocation('/domain', domainRoles) - .subLocation('/domain/ports', ['domain-port']) - .subLocation('/data-access', ['aggregate-repository', 'query-model-loader']) - .subLocation('/adapters/{adapter}', ['domain-port-adapter'], { - mayImportExternalPackages: false, - mayImportRoles: [ - 'domain-port', - 'external-client-error', - 'external-client-model', - 'external-client-service', - ], - }), - - location('src/platform') - .subLocation('/domain', domainRoles) - .subLocation('/infra', [], { mayImportRoles: [] }) - .subLocation('/infra/external-clients/{client}', externalClientRoles) - .subLocation('/infra/cli/input', ['generic-cli-input-parser']) - .subLocation('/infra/cli/presentation', cliPresentationRoles), - - location('src/entrypoint').subLocation('/_platform', entrypointRoles), - - location('src/shell', ['main', 'cli-error-handler']), - ], + unassignedPackages: ['apps/docs', 'apps/eclair'], }) diff --git a/.riviere/role-selection-guide.md b/.riviere/role-selection-guide.md index 3b57bb9b2..6f5e3b259 100644 --- a/.riviere/role-selection-guide.md +++ b/.riviere/role-selection-guide.md @@ -24,7 +24,6 @@ If yes, it is: - `cli-entrypoint`, or - a component used by the `cli-entrypoint` to process the raw inputs, such as: - `entrypoint-cli-input-parser` - - `generic-cli-input-parser` when the parsing is primitive technical machinery with no entrypoint meaning - `command-input-factory` ## 2. Loading previously stored state @@ -86,9 +85,9 @@ If yes, it is part of the query side. Ask: does it orchestrate the query, or doe - If it orchestrates (loads a query model, calls query methods, returns a result): `query-model-use-case` - If it is the query model itself (holds immutable state, exposes read-only methods): `query-model` - If it defines result types returned by the query model: `query-model` -- If it loads the query model from storage: `query-model-loader` +- If it loads the concrete result for an actual query use case from storage: `query-model-loader` - If it defines the input contract for a query use case: `query-model-use-case-input` -- If it is an error thrown during query operations: `query-model-error` +- If loading the query model fails: `data-access-error` **Critical distinction from commands:** If the code loads state but NEVER modifies or saves it, it belongs on the query side. The presence of a repository-like loading pattern does not automatically make something a `command-use-case` + `aggregate-repository`. @@ -98,9 +97,9 @@ If yes, it is part of the query side. Ask: does it orchestrate the query, or doe Keep the three responsibilities separate: -- A generic client under `platform/infra/external-clients/{client}/` knows only the external system's API and types. -- A `domain-port` under `domain/ports/` defines the capability the domain needs in domain language. -- A `domain-port-adapter` under `adapters/{client}/` implements one domain port using one generic client API. +- A generic client under the use-case package's `infra/external-clients/{client}/` knows only the external system's API and types. +- A `domain-port` in the subdomain's domain-model package defines the capability the domain needs in domain language. +- A `domain-port-adapter` under the use-case feature's `adapters/{client}/` implements one domain port using one generic client API. The adapter translates between the two contracts. It does not contain domain decisions, application orchestration, direct Node API calls, or third-party package calls. It must not coordinate multiple clients. The Node and third-party restriction is specific to this architecture's deliberate split between a domain-port adapter and a generic external client; it is not a claim that all adapters everywhere must avoid technology imports. See [`domain-port-adapter`](role-definitions/domain-port-adapter.md) for the concrete Oxlint and GitHub examples and the failure caused by combining the two roles. diff --git a/.riviere/roles.ts b/.riviere/roles.ts index 46b1f8351..8734a4b82 100644 --- a/.riviere/roles.ts +++ b/.riviere/roles.ts @@ -1,38 +1,4 @@ -import { createRoleFactory } from '@living-architecture/riviere-role-enforcement' - -type RoleName = - | 'aggregate' - | 'aggregate-repository' - | 'cli-entrypoint' - | 'cli-error' - | 'cli-error-handler' - | 'entrypoint-cli-input-parser' - | 'generic-cli-input-parser' - | 'cli-output-formatter' - | 'cli-response-formatter' - | 'cli-response-writer' - | 'command-input-factory' - | 'command-use-case' - | 'command-use-case-input' - | 'command-use-case-result' - | 'command-use-case-result-value' - | 'domain-error' - | 'domain-event' - | 'domain-port' - | 'domain-service' - | 'domain-port-adapter' - | 'external-client-error' - | 'external-client-model' - | 'external-client-service' - | 'main' - | 'query-model' - | 'query-model-error' - | 'query-model-loader' - | 'query-model-use-case' - | 'query-model-use-case-input' - | 'value-object' - -const role = createRoleFactory() +import { role } from '@living-architecture/riviere-role-enforcement-domain-model' export const allRoles = [ role('cli-entrypoint', { targets: ['function'] }), @@ -66,9 +32,10 @@ export const allRoles = [ role('external-client-service', { targets: ['function'] }), role('aggregate-repository', { targets: ['class'], - allowedOutputs: ['aggregate', 'domain-error'], + allowedOutputs: ['aggregate'], forbiddenDependencies: ['aggregate-repository'], }), + role('data-access-error', { targets: ['class'] }), role('aggregate', { targets: ['interface', 'type-alias', 'class'], minPublicMethods: 1, @@ -88,17 +55,19 @@ export const allRoles = [ ], }), role('value-object', { - targets: ['interface', 'type-alias', 'class'], - forbiddenCallableMembers: true, + targets: ['class'], + forbiddenCallableDataMembers: true, forbiddenSupertypes: ['Error'], requiredPrivateMembers: ['brand'], + requiresPrivateConstructor: true, + requiredStaticMethodNamePrefix: 'parse', requiresDataMembers: true, forbiddenDependencies: ['aggregate', 'domain-service'], }), role('domain-error', { targets: ['class'] }), role('domain-event', { targets: ['type-alias'], - nameMatches: '.*Event$', + mustBeDataStructure: true, }), role('domain-port', { targets: ['interface', 'type-alias'] }), role('domain-service', { targets: ['function', 'class'] }), @@ -121,16 +90,14 @@ export const allRoles = [ role('query-model', { targets: ['class', 'function', 'interface', 'type-alias'], }), - role('query-model-error', { targets: ['class'] }), role('query-model-loader', { targets: ['class'], - allowedOutputs: ['query-model', 'domain-error'], + allowedOutputs: ['query-model'], forbiddenDependencies: ['query-model-loader'], }), role('external-client-model', { targets: ['interface', 'type-alias', 'class'] }), role('external-client-error', { targets: ['class'] }), role('entrypoint-cli-input-parser', { targets: ['function'] }), - role('generic-cli-input-parser', { targets: ['function'] }), role('cli-error', { targets: ['class'] }), role('main', { targets: ['function'], @@ -141,6 +108,27 @@ export const allRoles = [ 'query-model-loader', ], }), + role('published-language-annotation', { + requiresDecoratorSignature: true, + }), + role('published-language-data-structure', { + mustBeDataStructure: true, + }), + role('published-language-field-name', { + requiresStringLiteralConstant: true, + }), + role('published-language-parser', { + returns: [ + { success: true, '*': 'published-language-schema' }, + { success: false, '*': '*' }, + ], + }), + role('published-language-schema', { + mustBeDataStructure: true, + }), + role('published-language-union', { + requiresUnion: true, + }), ] as const -export type { RoleName } +export type RoleName = (typeof allRoles)[number]['name'] diff --git a/CLAUDE.md b/CLAUDE.md index 1cb53f9dd..b6a5df6aa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,22 +11,31 @@ For planning, discovery, PRD, architecture, delivery planning, or future-project ## Monorepo Structure ```text -apps/ - Deployable applications (not published) -packages/ - Shared libraries (publishable to npm) +apps/ - Applications that aggregate subdomain use cases +packages/ - Subdomains split into domain-model, use-cases, and published-language packages +tools/ - Standalone app packages; their subdomain packages live under packages/ ``` Current packages: -- `living-architecture/riviere-query` - Browser-safe query library (no Node.js dependencies) -- `living-architecture/riviere-builder` - Node.js builder (depends on riviere-query) -- `living-architecture/riviere-cli` - CLI tool with binary "riviere" (depends on riviere-builder) -- `living-architecture/riviere-schema` - Riviere schema definitions -- `living-architecture/riviere-extract-config` - JSON Schema and validation for extraction config DSL -- `living-architecture/riviere-extract-conventions` - Decorators for marking architectural components (depends on riviere-extract-config) -- `living-architecture/riviere-extract-ts` - TypeScript component extractor using ts-morph for AST parsing (depends on riviere-extract-config) +- `packages/dev-workflow-v2/domain-model` - Maintainer workflow domain model +- `packages/dev-workflow-v2/use-cases` - Maintainer workflow commands and adapters +- `packages/riviere-builder/domain-model` - Browser-safe graph construction and querying domain model +- `packages/riviere-builder/use-cases` - Commands, queries and data access for graph building and querying +- `packages/riviere-schema/published-language` - Rivière graph contract +- `packages/riviere-extract-config/published-language` - Extraction config contract +- `packages/riviere-extract-conventions/published-language` - Annotations and ESLint integration for extraction conventions +- `packages/riviere-extract-ts/domain-model` - TypeScript extraction domain model using ts-morph +- `packages/riviere-extract-ts/use-cases` - TypeScript extraction commands and data access +- `packages/riviere-role-enforcement/domain-model` - Role-enforcement domain model and Oxlint plugin +- `packages/riviere-role-enforcement/use-cases` - Role-enforcement command, repository, adapter and external clients Apps: -- `living-architecture/eclair` - Web app for viewing your software architecture via Riviere a schema -- `living-architecture/docs` - Living architecture documentation website +- `apps/cli` - CLI entrypoints and composition shell +- `apps/eclair` - Web app for viewing your software architecture via a Rivière schema +- `apps/docs` - Living architecture documentation website + +Tools: +- `tools/dev-workflow-v2` - Maintainer workflow app and plugin entrypoints Key documents: - `docs/project/PRD/` - Current PRD folders @@ -35,7 +44,7 @@ Key documents: - `docs/architecture/domain-terminology/contextive/definitions.glossary.yml` - `docs/architecture/adr/` - Decision records -All code must follow the audit checklist in the [`development-skills:separation-of-concerns`](https://github.com/NTCoding/claude-skillz/blob/main/separation-of-concerns/SKILL.md) skill. +All code must follow [ADR-002](docs/architecture/adr/ADR-002-allowed-folder-structures.md) and the executable rules in [`.riviere/role-enforcement.config.ts`](.riviere/role-enforcement.config.ts). Keep those two files aligned. Use domain terminology from the contextive definitions. Do not invent new terms or use technical jargon when domain terminology exists. @@ -79,15 +88,15 @@ pnpm nx graph # Add backend application pnpm nx g @nx/node:application apps/[app-name] -# Add shared library (publishable) -pnpm nx g @nx/js:library packages/[pkg-name] --publishable --importPath=@living-architecture/[pkg-name] +# Add a subdomain package +pnpm nx g @nx/js:library packages/[subdomain]/[domain-model|use-cases|published-language] --publishable --importPath=@living-architecture/[package-name] ``` After generating a new project: -1. Update the project's package.json with correct name: `@living-architecture/[project-name]` +1. Update the project's package.json with the correct published package name 2. Create the 3-file tsconfig structure (tsconfig.json, tsconfig.lib.json, tsconfig.spec.json) 3. Add vitest.config.ts if tests are needed with 100% coverage as the default -4. If importing from another project, add `"@living-architecture/[pkg-name]": "workspace:*"` to dependencies +4. If importing from another project, add its published package name with `"workspace:*"` to dependencies 5. Run `pnpm nx sync` to update TypeScript project references 6. Update this CLAUDE.md "Current packages" section diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b763907f9..3e378fd7c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,8 +17,11 @@ This workflow is deliberately independent of Claude Code, Codex, OpenCode, Kimi, ## Development Setup ```bash -pnpm install -nx run-many -t build # Verify setup works +nvm install +nvm use +corepack enable +pnpm install --frozen-lockfile +pnpm nx run-many -t build # Verify setup works ``` ## Commands @@ -54,7 +57,7 @@ chore: update dependencies Follow the conventions in [`docs/conventions/`](docs/conventions/): - [`software-design.md`](docs/conventions/software-design.md) — Design principles - [`testing.md`](docs/conventions/testing.md) — Testing requirements -- Code placement follows the [`development-skills:separation-of-concerns`](https://github.com/NTCoding/claude-skillz/blob/main/separation-of-concerns/SKILL.md) skill +- Code placement follows [`ADR-002`](docs/architecture/adr/ADR-002-allowed-folder-structures.md) and the executable rules in [`.riviere/role-enforcement.config.ts`](.riviere/role-enforcement.config.ts) ## Testing Requirements @@ -83,7 +86,3 @@ If your harness supports agents, you can run these against your changes: External contributors use this lightweight public workflow: make the change and raise a detailed pull request. Members of the maintainer team use the full [`maintainer workflow`](docs/workflow/task-workflow.md), including planning, GitHub issues and the development harness. - -## AI-Assisted Development - -This project was built with [Claude Code](https://claude.com/claude-code) with skills from [claude-skillz](https://github.com/NTCoding/claude-skillz). diff --git a/README.md b/README.md index 6b3eb3dfd..a54f5fe8a 100644 --- a/README.md +++ b/README.md @@ -84,15 +84,14 @@ See the [extraction guide](https://living-architecture.dev/extract/) for AI-assi | Package | Purpose | Install | |---------|---------|---------| -| `@living-architecture/riviere-schema` | Schema definition and validation | `npm i @living-architecture/riviere-schema` | -| `@living-architecture/riviere-query` | Query graphs. Browser-safe. | `npm i @living-architecture/riviere-query` | -| `@living-architecture/riviere-builder` | Build graphs programmatically | `npm i @living-architecture/riviere-builder` | +| `@living-architecture/riviere-schema-published-language` | Schema definition and validation | `npm i @living-architecture/riviere-schema-published-language` | +| `@living-architecture/riviere-builder-domain-model` | Build and query graphs programmatically | `npm i @living-architecture/riviere-builder-domain-model` | | `@living-architecture/riviere-cli` | CLI for extraction workflows | `npm i -g @living-architecture/riviere-cli` | ## Build a Graph ```typescript -import { RiviereBuilder } from '@living-architecture/riviere-builder'; +import { RiviereBuilder } from '@living-architecture/riviere-builder-domain-model'; const builder = RiviereBuilder.new({ sources: [{ repository: 'https://github.com/your-org/your-repo' }], @@ -126,7 +125,7 @@ const graph = builder.build(); ## Query a Graph ```typescript -import { RiviereQuery } from '@living-architecture/riviere-query'; +import { RiviereQuery } from '@living-architecture/riviere-builder-domain-model'; const query = RiviereQuery.fromJSON(graphData); @@ -145,7 +144,7 @@ const events = query.componentsByType('Event'); ## The Rivière Schema -Rivière graphs are JSON documents conforming to the [Rivière schema](./packages/riviere-schema/riviere.schema.json). +Rivière graphs are JSON documents conforming to the [Rivière schema](./packages/riviere-schema/published-language/riviere.schema.json). ```json { @@ -159,7 +158,7 @@ Rivière graphs are JSON documents conforming to the [Rivière schema](./package } ``` -See [examples](./packages/riviere-schema/examples/) for complete multi-domain graphs. +See [examples](./packages/riviere-schema/published-language/examples/) for complete multi-domain graphs. ## Visualize with Éclair diff --git a/apps/cli/CLAUDE.md b/apps/cli/CLAUDE.md new file mode 100644 index 000000000..898b9ea89 --- /dev/null +++ b/apps/cli/CLAUDE.md @@ -0,0 +1,28 @@ +# riviere-cli + +CLI tool for building and querying Rivière architecture graphs. + +Architecture defined in [ADR-002](../../docs/architecture/adr/ADR-002-allowed-folder-structures.md). + +## Workflow Prompts + +The `docs/workflow/` directory contains AI extraction workflow prompts (step-1 through step-6). These prompts reference CLI commands directly. + +**When modifying CLI commands, update the corresponding workflow prompts.** + +If a command's flags, behavior, or output format changes, ensure the workflow prompts still work correctly. This keeps the extraction workflow in sync with the CLI. + +## Design Philosophy + +Default to the most reliable, powerful behavior. Opt-out flags (`--no-*`) for edge cases. Users should get the best experience by default, not discover it when things don't work. + +## Location Structure + +Follow [ADR-002](../../docs/architecture/adr/ADR-002-allowed-folder-structures.md) and the executable rules in [`.riviere/role-enforcement.config.ts`](../../.riviere/role-enforcement.config.ts). + +## Commands + +- `riviere builder ` - Graph building commands +- `riviere query ` - Graph query commands + +Run `nx generate-docs riviere-cli` to regenerate CLI reference documentation after command changes. diff --git a/packages/riviere-cli/README.md b/apps/cli/README.md similarity index 100% rename from packages/riviere-cli/README.md rename to apps/cli/README.md diff --git a/packages/riviere-cli/docs/generated/cli-reference.md b/apps/cli/docs/generated/cli-reference.md similarity index 100% rename from packages/riviere-cli/docs/generated/cli-reference.md rename to apps/cli/docs/generated/cli-reference.md diff --git a/packages/riviere-cli/docs/workflow/step-1-understand.md b/apps/cli/docs/workflow/step-1-understand.md similarity index 100% rename from packages/riviere-cli/docs/workflow/step-1-understand.md rename to apps/cli/docs/workflow/step-1-understand.md diff --git a/packages/riviere-cli/docs/workflow/step-2-define-components.md b/apps/cli/docs/workflow/step-2-define-components.md similarity index 100% rename from packages/riviere-cli/docs/workflow/step-2-define-components.md rename to apps/cli/docs/workflow/step-2-define-components.md diff --git a/packages/riviere-cli/docs/workflow/step-3-extract.md b/apps/cli/docs/workflow/step-3-extract.md similarity index 100% rename from packages/riviere-cli/docs/workflow/step-3-extract.md rename to apps/cli/docs/workflow/step-3-extract.md diff --git a/packages/riviere-cli/docs/workflow/step-4-link.md b/apps/cli/docs/workflow/step-4-link.md similarity index 98% rename from packages/riviere-cli/docs/workflow/step-4-link.md rename to apps/cli/docs/workflow/step-4-link.md index 7db9059ba..33e3724a8 100644 --- a/packages/riviere-cli/docs/workflow/step-4-link.md +++ b/apps/cli/docs/workflow/step-4-link.md @@ -85,7 +85,7 @@ The link is **API → UseCase**. **Fetch the CLI reference for full command syntax and examples:** ```text -https://raw.githubusercontent.com/NTCoding/living-architecture/main/packages/riviere-cli/docs/generated/cli-reference.md +https://raw.githubusercontent.com/NTCoding/living-architecture/main/apps/cli/docs/generated/cli-reference.md ``` | Command | When to Use | diff --git a/packages/riviere-cli/docs/workflow/step-5-enrich.md b/apps/cli/docs/workflow/step-5-enrich.md similarity index 98% rename from packages/riviere-cli/docs/workflow/step-5-enrich.md rename to apps/cli/docs/workflow/step-5-enrich.md index 691d1e861..a2ab3a1a7 100644 --- a/packages/riviere-cli/docs/workflow/step-5-enrich.md +++ b/apps/cli/docs/workflow/step-5-enrich.md @@ -86,7 +86,7 @@ emit(new OrderPlaced(...)); // emits: "OrderPlaced event" **Fetch the CLI reference for `enrich` command syntax:** ```text -https://raw.githubusercontent.com/NTCoding/living-architecture/main/packages/riviere-cli/docs/generated/cli-reference.md +https://raw.githubusercontent.com/NTCoding/living-architecture/main/apps/cli/docs/generated/cli-reference.md ``` **Best effort on all fields.** Add every value you identify — state changes, business rules, reads, validates, modifies, emits. All options are repeatable. diff --git a/packages/riviere-cli/docs/workflow/step-6-validate.md b/apps/cli/docs/workflow/step-6-validate.md similarity index 100% rename from packages/riviere-cli/docs/workflow/step-6-validate.md rename to apps/cli/docs/workflow/step-6-validate.md diff --git a/apps/cli/esbuild.config.mjs b/apps/cli/esbuild.config.mjs new file mode 100644 index 000000000..9cca3f5bd --- /dev/null +++ b/apps/cli/esbuild.config.mjs @@ -0,0 +1,60 @@ +import * as esbuild from 'esbuild' +import { readFileSync } from 'node:fs' +import { + dirname, + join, +} from 'node:path' +import { fileURLToPath } from 'node:url' + +// Resolve package.json relative to this config file, not CWD +const __dirname = dirname(fileURLToPath(import.meta.url)) +const pkg = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf-8')) + +const externalDependencies = Object.keys(pkg.dependencies || {}) + .filter(dep => !dep.startsWith('@living-architecture/')) +const executableBanner = [ + '#!/usr/bin/env node', + "import { createRequire as __createRequire } from 'node:module';", + "import { fileURLToPath as __fileURLToPath } from 'node:url';", + "import { dirname as __pathDirname } from 'node:path';", + 'const require = __createRequire(import.meta.url);', + 'const __filename = __fileURLToPath(import.meta.url);', + 'const __dirname = __pathDirname(__filename);', +].join('\n') + +// CLI binary entry point +await esbuild.build({ + entryPoints: ['src/shell/bin.ts'], + bundle: true, + platform: 'node', + target: 'node18', + format: 'esm', + outfile: 'dist/bin.js', + banner: {js: executableBanner,}, + external: externalDependencies, + define: { INJECTED_VERSION: JSON.stringify(pkg.version) }, +}) + +await esbuild.build({ + entryPoints: ['src/shell/role-enforcement-bin.ts'], + bundle: true, + platform: 'node', + target: 'node18', + format: 'esm', + outfile: 'dist/role-enforcement-bin.js', + banner: {js: executableBanner,}, + external: externalDependencies, +}) + +// Library entry point (no side effects) +await esbuild.build({ + entryPoints: ['src/index.ts'], + bundle: true, + platform: 'node', + target: 'node18', + format: 'esm', + outfile: 'dist/index.js', + banner: {js: executableBanner,}, + external: externalDependencies, + define: { INJECTED_VERSION: JSON.stringify(pkg.version) }, +}) diff --git a/apps/cli/package.json b/apps/cli/package.json new file mode 100644 index 000000000..e8346efc0 --- /dev/null +++ b/apps/cli/package.json @@ -0,0 +1,47 @@ +{ + "name": "@living-architecture/riviere-cli", + "version": "0.11.3", + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/NTCoding/living-architecture.git", + "directory": "apps/cli" + }, + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "@living-architecture/source": "./src/index.ts", + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "files": [ + "dist", + "!dist/**/__fixtures__/**", + "!**/*.tsbuildinfo" + ], + "bin": { + "riviere": "./dist/bin.js", + "riviere-role-enforcement": "./dist/role-enforcement-bin.js" + }, + "engines": { + "node": ">=20" + }, + "dependencies": { + "@living-architecture/riviere-builder-use-cases": "workspace:*", + "@living-architecture/riviere-extract-ts-domain-model": "workspace:*", + "@living-architecture/riviere-extract-ts-use-cases": "workspace:*", + "@living-architecture/riviere-role-enforcement-domain-model": "workspace:*", + "@living-architecture/riviere-role-enforcement-use-cases": "workspace:*", + "@living-architecture/riviere-schema-published-language": "workspace:*", + "commander": "^14.0.2", + "zod": "^4.3.5" + } +} diff --git a/apps/cli/project.json b/apps/cli/project.json new file mode 100644 index 000000000..ed3dc8c53 --- /dev/null +++ b/apps/cli/project.json @@ -0,0 +1,55 @@ +{ + "name": "riviere-cli", + "targets": { + "build": { + "executor": "nx:run-commands", + "options": { + "commands": [ + "node esbuild.config.mjs", + "tsc --emitDeclarationOnly --declaration --declarationDir dist" + ], + "cwd": "{projectRoot}", + "parallel": false + }, + "outputs": ["{projectRoot}/dist"] + }, + "generate-docs": { + "executor": "nx:run-commands", + "options": { + "command": "pnpm exec npx tsx apps/cli/scripts/generate-docs.ts", + "cwd": "{workspaceRoot}" + }, + "dependsOn": ["build"], + "outputs": ["{projectRoot}/docs/generated"] + }, + "check-generated-docs": { + "executor": "nx:run-commands", + "options": { + "commands": [ + "pnpm exec npx tsx apps/cli/scripts/generate-docs.ts", + "git diff --exit-code apps/cli/docs/generated/ || (echo 'Generated docs are stale. Run: pnpm nx generate-docs riviere-cli' && exit 1)" + ], + "cwd": "{workspaceRoot}", + "parallel": false + }, + "dependsOn": ["build"] + }, + "role-check": { + "executor": "nx:run-commands", + "dependsOn": ["^build"], + "options": { + "command": "pnpm exec tsx apps/cli/src/shell/role-enforcement-bin.ts .riviere/role-enforcement.config.ts --package apps/cli", + "cwd": "{workspaceRoot}" + } + }, + "smoke-test": { + "executor": "nx:run-commands", + "options": { + "commands": ["node dist/bin.js --help", "node dist/bin.js extract --help"], + "cwd": "{projectRoot}", + "parallel": false + }, + "dependsOn": ["build"] + } + } +} diff --git a/apps/cli/scripts/generate-docs.ts b/apps/cli/scripts/generate-docs.ts new file mode 100644 index 000000000..a0b6e085a --- /dev/null +++ b/apps/cli/scripts/generate-docs.ts @@ -0,0 +1,339 @@ +#!/usr/bin/env tsx +/** + * CLI Reference Documentation Generator + * + * Generates markdown documentation from Commander.js command definitions. + * Run with: pnpm exec tsx scripts/generate-docs.ts + */ + +import type { Command, Argument } from 'commander' +import { writeFileSync, mkdirSync } from 'node:fs' +import { join } from 'node:path' +import { createProgram } from '../src/shell/cli' + +interface OptionInfo { + flags: string + description: string + required: boolean +} + +interface ArgumentInfo { + name: string + description: string + required: boolean +} + +interface CommandInfo { + name: string + fullName: string + description: string + options: OptionInfo[] + arguments: ArgumentInfo[] + examples: readonly string[] +} + +interface CommandWithRegisteredArgs { + readonly registeredArguments?: readonly Argument[] +} + +interface CommandWithEvents { + readonly _events?: { readonly afterHelp?: unknown } +} + +type AfterHelpContext = { + readonly command: Command + readonly write: (str: string) => void +} + +function extractOptions(cmd: Command): readonly OptionInfo[] { + return cmd.options + .filter((opt) => opt.long !== '--help' && opt.long !== '--version') + .map((opt) => ({ + flags: opt.flags, + description: opt.description ?? '(no description)', + required: opt.mandatory, + })) +} + +function hasRegisteredArguments(cmd: Command): cmd is Command & CommandWithRegisteredArgs { + return 'registeredArguments' in cmd && Array.isArray(cmd.registeredArguments) +} + +function extractArguments(cmd: Command): readonly ArgumentInfo[] { + if (!hasRegisteredArguments(cmd)) { + return [] + } + + return cmd.registeredArguments.map((arg) => ({ + name: arg.name(), + description: arg.description, + required: arg.required, + })) +} + +function isAfterHelpCallback(fn: unknown): fn is (ctx: AfterHelpContext) => string | void { + return typeof fn === 'function' +} + +function callAfterHelpHandler(afterHelp: unknown, cmd: Command): string { + if (isAfterHelpCallback(afterHelp)) { + const parts: string[] = [] + const context: AfterHelpContext = { + command: cmd, + write: (str: string) => { + parts.push(str) + }, + } + const result = afterHelp(context) + return typeof result === 'string' ? result : parts.join('') + } + + if (typeof afterHelp === 'string') { + return afterHelp + } + + return '' +} + +function parseExamplesFromHelpText(helpText: string): readonly string[] { + const lines = helpText.split('\n') + const examples: string[] = [] + const examplesStartIndex = lines.findIndex((line) => line.trim().startsWith('Examples:')) + + if (examplesStartIndex === -1) { + return examples + } + + const exampleLines = lines.slice(examplesStartIndex + 1) + const exampleParts: string[] = [] + + for (const line of exampleLines) { + const trimmed = line.trim() + const isNewExample = trimmed.startsWith('$') || trimmed.startsWith('#') + const isContinuation = trimmed && exampleParts.length > 0 && !isNewExample + const isBlankAfterExample = !trimmed && exampleParts.length > 0 + + if (isNewExample) { + if (exampleParts.length > 0) { + examples.push(exampleParts.join('\n ').trim()) + exampleParts.length = 0 + } + exampleParts.push(trimmed) + } else if (isContinuation) { + exampleParts.push(trimmed) + } else if (isBlankAfterExample) { + examples.push(exampleParts.join('\n ').trim()) + exampleParts.length = 0 + } + } + + if (exampleParts.length > 0) { + examples.push(exampleParts.join('\n ').trim()) + } + + return examples +} + +function hasEvents(cmd: Command): cmd is Command & CommandWithEvents { + return '_events' in cmd && typeof cmd._events === 'object' && cmd._events !== null +} + +function extractExamples(cmd: Command): readonly string[] { + if (!hasEvents(cmd)) { + return [] + } + + const afterHelp = cmd._events?.afterHelp + if (!afterHelp) { + return [] + } + + try { + const helpText = callAfterHelpHandler(afterHelp, cmd) + return helpText ? parseExamplesFromHelpText(helpText) : [] + } catch { + return [] + } +} + +function extractCommandInfo(cmd: Command, parentName: string): CommandInfo { + const fullName = parentName ? `${parentName} ${cmd.name()}` : cmd.name() + + return { + name: cmd.name(), + fullName, + description: cmd.description() ?? '(no description)', + options: [...extractOptions(cmd)], + arguments: [...extractArguments(cmd)], + examples: extractExamples(cmd), + } +} + +function collectCommands(cmd: Command, parentName: string): readonly CommandInfo[] { + const fullName = parentName ? `${parentName} ${cmd.name()}` : cmd.name() + + return cmd.commands.flatMap((subcmd) => + subcmd.commands.length > 0 + ? collectCommands(subcmd, fullName) + : [extractCommandInfo(subcmd, fullName)], + ) +} + +function formatOption(opt: OptionInfo): string { + return `| \`${opt.flags}\` | ${opt.description} |` +} + +function generateCommandMarkdown(cmd: CommandInfo): string { + const lines: string[] = [] + + lines.push(`### \`${cmd.name}\``) + lines.push('') + lines.push(cmd.description) + lines.push('') + + const args = cmd.arguments.map((a) => (a.required ? `<${a.name}>` : `[${a.name}]`)).join(' ') + const syntax = `${cmd.fullName}${args ? ' ' + args : ''} [options]` + lines.push('```bash') + lines.push(syntax) + lines.push('```') + lines.push('') + + if (cmd.arguments.length > 0) { + lines.push('**Arguments:**') + lines.push('| Argument | Description |') + lines.push('|----------|-------------|') + for (const arg of cmd.arguments) { + lines.push(`| \`<${arg.name}>\` | ${arg.description} |`) + } + lines.push('') + } + + const requiredOpts = cmd.options.filter((o) => o.required) + if (requiredOpts.length > 0) { + lines.push('**Required:**') + lines.push('| Flag | Description |') + lines.push('|------|-------------|') + for (const opt of requiredOpts) { + lines.push(formatOption(opt)) + } + lines.push('') + } + + const optionalOpts = cmd.options.filter((o) => !o.required) + if (optionalOpts.length > 0) { + lines.push('**Optional:**') + lines.push('| Flag | Description |') + lines.push('|------|-------------|') + for (const opt of optionalOpts) { + lines.push(formatOption(opt)) + } + lines.push('') + } + + if (cmd.examples.length > 0) { + lines.push('**Examples:**') + lines.push('```bash') + for (const example of cmd.examples) { + const cleaned = example.replace(/^\$\s*/, '').replace(/^#\s*/, '# ') + lines.push(cleaned) + } + lines.push('```') + lines.push('') + } + + lines.push('---') + lines.push('') + + return lines.join('\n') +} + +function generateReference(): string { + const program = createProgram() + const allCommands = collectCommands(program, '') + + const builderCommands = allCommands.filter((c) => c.fullName.startsWith('riviere builder')) + const queryCommands = allCommands.filter((c) => c.fullName.startsWith('riviere query')) + const extractCommands = allCommands.filter((c) => c.fullName.startsWith('riviere extract')) + + const lines: string[] = [] + + lines.push('---') + lines.push('pageClass: reference') + lines.push('---') + lines.push('') + lines.push('# CLI Command Reference') + lines.push('') + lines.push('Complete documentation for all Riviere CLI commands.') + lines.push('') + + lines.push('## Installation') + lines.push('') + lines.push('```bash') + lines.push('npm install @living-architecture/riviere-cli') + lines.push('```') + lines.push('') + + lines.push('## Usage') + lines.push('') + lines.push('```bash') + lines.push('riviere builder [options] # Graph building commands') + lines.push('riviere query [options] # Graph query commands') + lines.push('riviere extract [options] # Component extraction commands') + lines.push('```') + lines.push('') + + lines.push('## Exit Codes') + lines.push('') + lines.push('- `0`: Success (including warnings)') + lines.push('- `1`: Error or failed validation/consistency') + lines.push('') + lines.push('---') + lines.push('') + + lines.push('## Builder Commands') + lines.push('') + lines.push('Commands for constructing architecture graphs.') + lines.push('') + + for (const cmd of builderCommands) { + lines.push(generateCommandMarkdown(cmd)) + } + + lines.push('## Query Commands') + lines.push('') + lines.push('Commands for analyzing and querying graphs.') + lines.push('') + + for (const cmd of queryCommands) { + lines.push(generateCommandMarkdown(cmd)) + } + + lines.push('## Extract Commands') + lines.push('') + lines.push('Commands for extracting architectural components from source code.') + lines.push('') + + for (const cmd of extractCommands) { + lines.push(generateCommandMarkdown(cmd)) + } + + lines.push('## See Also') + lines.push('') + lines.push('- [CLI Quick Start](/get-started/cli-quick-start)') + lines.push('- [Extraction Workflow](/extract/)') + lines.push('- [Graph Structure](/reference/schema/graph-structure)') + lines.push('') + + return lines.join('\n') +} + +const outputDir = join(import.meta.dirname, '..', 'docs', 'generated') +const outputPath = join(outputDir, 'cli-reference.md') + +mkdirSync(outputDir, { recursive: true }) + +const content = generateReference() +writeFileSync(outputPath, content, 'utf-8') + +console.log(`Generated: ${outputPath}`) +console.log(`Lines: ${content.split('\n').length}`) diff --git a/packages/riviere-cli/src/platform/__fixtures__/add-component-fixtures.ts b/apps/cli/src/__fixtures__/add-component-fixtures.ts similarity index 100% rename from packages/riviere-cli/src/platform/__fixtures__/add-component-fixtures.ts rename to apps/cli/src/__fixtures__/add-component-fixtures.ts diff --git a/apps/cli/src/__fixtures__/command-test-fixtures.ts b/apps/cli/src/__fixtures__/command-test-fixtures.ts new file mode 100644 index 000000000..b84f95c8d --- /dev/null +++ b/apps/cli/src/__fixtures__/command-test-fixtures.ts @@ -0,0 +1,372 @@ +import { execFileSync } from 'node:child_process' +import { mkdtemp, rm, mkdir, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { vi, beforeEach, afterEach, expect, it } from 'vitest' +import { createProgram } from '../shell/cli' +import { handleGlobalError } from '../shell/global-error-handler' + +class ProcessExitError extends Error { + constructor(public exitCode: number) { + super(`process.exit(${exitCode})`) + this.name = 'ProcessExitError' + } +} + +export class TestAssertionError extends Error { + constructor(message: string) { + super(message) + this.name = 'TestAssertionError' + } +} + +export class MockError extends Error { + constructor(message: string) { + super(message) + this.name = 'MockError' + } +} + +export interface ErrorOutput { + success: false + error: { + code: string + message: string + suggestions: string[] + } +} + +function isErrorOutput(value: unknown): value is ErrorOutput { + if (typeof value !== 'object' || value === null) return false + if (!('success' in value) || value.success !== false) return false + if (!('error' in value) || typeof value.error !== 'object' || value.error === null) return false + return true +} + +export function parseErrorOutput(consoleOutput: string[]): ErrorOutput { + const firstLine = consoleOutput[0] + if (firstLine === undefined) { + throw new TestAssertionError('Expected console output but got empty array') + } + const parsed: unknown = JSON.parse(firstLine) + if (!isErrorOutput(parsed)) { + throw new TestAssertionError('Invalid error output') + } + return parsed +} + +export function parseSuccessOutput( + consoleOutput: string[], + guard: (value: unknown) => value is T, + errorMessage: string, +): T { + const firstLine = consoleOutput[0] + if (firstLine === undefined) { + throw new TestAssertionError('Expected console output but got empty array') + } + const parsed: unknown = JSON.parse(firstLine) + if (!guard(parsed)) { + throw new TestAssertionError(errorMessage) + } + return parsed +} + +export interface TestContext { + testDir: string + originalCwd: string + consoleOutput: string[] +} + +export function createTestContext(): TestContext { + return { + testDir: '', + originalCwd: '', + consoleOutput: [], + } +} + +export function runIsolatedGit(directory: string, args: string[]): void { + const env = { ...process.env } + for (const name of Object.keys(env)) { + if (name.startsWith('GIT_')) delete env[name] + } + execFileSync('/usr/bin/git', args, { cwd: directory, env, stdio: 'ignore' }) +} + +export function setupCommandTest(ctx: TestContext): void { + beforeEach(async () => { + ctx.testDir = await mkdtemp(join(tmpdir(), 'riviere-test-')) + ctx.originalCwd = process.cwd() + ctx.consoleOutput = [] + process.chdir(ctx.testDir) + ctx.testDir = process.cwd() + runIsolatedGit(ctx.testDir, ['init', '--initial-branch=main']) + runIsolatedGit(ctx.testDir, ['config', 'user.email', 'test@example.com']) + runIsolatedGit(ctx.testDir, ['config', 'user.name', 'Test User']) + runIsolatedGit(ctx.testDir, ['remote', 'add', 'origin', 'https://github.com/test/repo.git']) + vi.spyOn(console, 'log').mockImplementation((msg: string) => ctx.consoleOutput.push(msg)) + vi.spyOn(process, 'exit').mockImplementation((code?: string | number | null | undefined) => { + throw new ProcessExitError(typeof code === 'number' ? code : 0) + }) + }) + + afterEach(async () => { + vi.restoreAllMocks() + process.chdir(ctx.originalCwd) + await rm(ctx.testDir, { recursive: true }) + }) +} + +export async function createGraph( + testDir: string, + graphData: object, + subPath = '.riviere', +): Promise { + const graphDir = join(testDir, subPath) + await mkdir(graphDir, { recursive: true }) + const graphPath = join(graphDir, 'graph.json') + await writeFile(graphPath, JSON.stringify(graphData), 'utf-8') + return graphPath +} + +export const baseMetadata = { + sources: [{ repository: 'https://github.com/org/repo' }], + domains: { + orders: { + description: 'Order management', + systemType: 'domain', + }, + }, +} + +export const sourceLocation = { + repository: 'https://github.com/org/repo', + filePath: 'src/orders/handler.ts', +} + +export const useCaseComponent = { + id: 'orders:checkout:usecase:place-order', + type: 'UseCase', + name: 'place-order', + domain: 'orders', + module: 'checkout', + sourceLocation, +} + +export const apiComponent = { + id: 'orders:checkout:api:place-order', + type: 'API', + name: 'place-order', + domain: 'orders', + module: 'checkout', + sourceLocation, + apiType: 'REST', + httpMethod: 'POST', + path: '/orders', +} + +export const eventHandlerComponent = { + id: 'orders:checkout:eventhandler:on-order-placed', + type: 'EventHandler', + name: 'on-order-placed', + domain: 'orders', + module: 'checkout', + sourceLocation, + subscribedEvents: ['OrderPlaced'], +} + +export const validLink = { + id: 'orders:checkout:api:place-order→orders:checkout:usecase:place-order:sync', + source: 'orders:checkout:api:place-order', + target: 'orders:checkout:usecase:place-order', + type: 'sync', +} + +export async function createGraphWithDomain(testDir: string, domainName: string): Promise { + const graphDir = join(testDir, '.riviere') + await mkdir(graphDir, { recursive: true }) + const graph = { + version: '1.0', + metadata: { + sources: [{ repository: 'https://github.com/org/repo' }], + domains: { + [domainName]: { + description: 'Test domain', + systemType: 'domain', + }, + }, + }, + components: [], + links: [], + } + await writeFile(join(graphDir, 'graph.json'), JSON.stringify(graph), 'utf-8') +} + +export async function createGraphWithSource(testDir: string, repository: string): Promise { + const graphDir = join(testDir, '.riviere') + await mkdir(graphDir, { recursive: true }) + const graph = { + version: '1.0', + metadata: { + sources: [{ repository }], + domains: { + orders: { + description: 'Orders', + systemType: 'domain', + }, + }, + }, + components: [], + links: [], + } + await writeFile(join(graphDir, 'graph.json'), JSON.stringify(graph), 'utf-8') +} + +export async function createGraphWithComponent(testDir: string, component: object): Promise { + const graphDir = join(testDir, '.riviere') + await mkdir(graphDir, { recursive: true }) + const graph = { + version: '1.0', + metadata: { + sources: [{ repository: 'https://github.com/org/repo' }], + domains: { + orders: { + description: 'Order management', + systemType: 'domain', + }, + }, + }, + components: [component], + links: [], + } + await writeFile(join(graphDir, 'graph.json'), JSON.stringify(graph), 'utf-8') +} + +export interface CustomTypeDefinition { + description?: string + requiredProperties?: Record< + string, + { + type: string + description?: string + } + > + optionalProperties?: Record< + string, + { + type: string + description?: string + } + > +} + +export async function createGraphWithCustomType( + testDir: string, + domainName: string, + customTypeName: string, + customTypeDefinition: CustomTypeDefinition, +): Promise { + const graphDir = join(testDir, '.riviere') + await mkdir(graphDir, { recursive: true }) + const graph = { + version: '1.0', + metadata: { + sources: [{ repository: 'https://github.com/org/repo' }], + domains: { + [domainName]: { + description: 'Test domain', + systemType: 'domain', + }, + }, + customTypes: { [customTypeName]: customTypeDefinition }, + }, + components: [], + links: [], + } + await writeFile(join(graphDir, 'graph.json'), JSON.stringify(graph), 'utf-8') +} + +export const domainOpComponent = { + id: 'orders:checkout:domainop:confirm-order', + type: 'DomainOp', + name: 'Confirm Order', + domain: 'orders', + module: 'checkout', + operationName: 'confirmOrder', + sourceLocation: { + repository: 'https://github.com/org/repo', + filePath: 'src/domain.ts', + }, +} + +export const simpleUseCaseComponent = { + id: 'orders:checkout:usecase:place-order', + type: 'UseCase', + name: 'Place Order', + domain: 'orders', + module: 'checkout', + sourceLocation: { + repository: 'https://github.com/org/repo', + filePath: 'src/usecase.ts', + }, +} + +export function hasSuccessOutputStructure(value: unknown): value is { + success: true + data: object +} { + if (typeof value !== 'object' || value === null) return false + if (!('success' in value) || value.success !== true) return false + if (!('data' in value) || typeof value.data !== 'object' || value.data === null) return false + return true +} + +export function testCommandRegistration(commandName: string): void { + it(`registers ${commandName} command under builder`, () => { + const program = createProgram() + const builderCmd = program.commands.find((cmd) => cmd.name() === 'builder') + const cmd = builderCmd?.commands.find((cmd) => cmd.name() === commandName) + expect(cmd?.name()).toBe(commandName) + }) +} + +export async function testCustomGraphPath( + ctx: TestContext, + commandArgs: string[], + parseOutput: (consoleOutput: string[]) => T, +): Promise { + const customPath = await createGraph( + ctx.testDir, + { + version: '1.0', + metadata: baseMetadata, + components: [], + links: [], + }, + 'custom', + ) + + await createProgram().parseAsync([ + 'node', + 'riviere', + ...commandArgs, + '--graph', + customPath, + '--json', + ]) + return parseOutput(ctx.consoleOutput) +} + +export function parseCommandWithErrorHandling(args: string[]): Promise { + return createProgram().parseAsync(args).catch(handleGlobalError) +} + +export function assertDefined( + value: T | undefined | null, + message = 'Expected value to be defined', +): T { + if (value === undefined || value === null) { + throw new TestAssertionError(message) + } + return value +} diff --git a/packages/riviere-cli/src/features/builder/entrypoint/_platform/cli/option-collectors.ts b/apps/cli/src/features/builder/entrypoint/_platform/cli/option-collectors.ts similarity index 100% rename from packages/riviere-cli/src/features/builder/entrypoint/_platform/cli/option-collectors.ts rename to apps/cli/src/features/builder/entrypoint/_platform/cli/option-collectors.ts diff --git a/packages/riviere-cli/src/features/builder/entrypoint/add-component/add-component-custom.spec.ts b/apps/cli/src/features/builder/entrypoint/add-component/add-component-custom.spec.ts similarity index 94% rename from packages/riviere-cli/src/features/builder/entrypoint/add-component/add-component-custom.spec.ts rename to apps/cli/src/features/builder/entrypoint/add-component/add-component-custom.spec.ts index 24f3cc930..a49017f3a 100644 --- a/packages/riviere-cli/src/features/builder/entrypoint/add-component/add-component-custom.spec.ts +++ b/apps/cli/src/features/builder/entrypoint/add-component/add-component-custom.spec.ts @@ -1,16 +1,14 @@ -import { - describe, it, expect -} from 'vitest' +import { describe, it, expect } from 'vitest' import { readFile } from 'node:fs/promises' import { join } from 'node:path' import { createProgram } from '../../../../shell/cli' -import { CliErrorCode } from '../../../../platform/infra/cli/presentation/error-codes' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' import { type TestContext, createTestContext, setupCommandTest, createGraphWithCustomType, -} from '../../../../platform/__fixtures__/command-test-fixtures' +} from '../../../../__fixtures__/command-test-fixtures' describe('riviere builder add-component Custom type', () => { const ctx: TestContext = createTestContext() diff --git a/packages/riviere-cli/src/features/builder/entrypoint/add-component/add-component-options.ts b/apps/cli/src/features/builder/entrypoint/add-component/add-component-options.ts similarity index 100% rename from packages/riviere-cli/src/features/builder/entrypoint/add-component/add-component-options.ts rename to apps/cli/src/features/builder/entrypoint/add-component/add-component-options.ts diff --git a/apps/cli/src/features/builder/entrypoint/add-component/add-component.spec.ts b/apps/cli/src/features/builder/entrypoint/add-component/add-component.spec.ts new file mode 100644 index 000000000..d67c17212 --- /dev/null +++ b/apps/cli/src/features/builder/entrypoint/add-component/add-component.spec.ts @@ -0,0 +1,396 @@ +import { describe, it, expect } from 'vitest' +import { readFile } from 'node:fs/promises' +import { join } from 'node:path' +import { createProgram } from '../../../../shell/cli' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' +import { + type TestContext, + assertDefined, + createTestContext, + setupCommandTest, + createGraphWithDomain, + MockError, +} from '../../../../__fixtures__/command-test-fixtures' +import { buildAddComponentArgs } from '../../../../__fixtures__/add-component-fixtures' + +function getErrorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message + } + return 'Unknown error' +} + +describe('riviere builder add-component', () => { + describe('command registration', () => { + it('registers add-component command under builder', () => { + const program = createProgram() + const builderCmd = program.commands.find((cmd) => cmd.name() === 'builder') + const addComponentCmd = builderCmd?.commands.find((cmd) => cmd.name() === 'add-component') + + expect(addComponentCmd?.name()).toBe('add-component') + }) + }) + + describe('error handling', () => { + const ctx: TestContext = createTestContext() + setupCommandTest(ctx) + + it('returns GRAPH_NOT_FOUND when no graph exists', async () => { + const program = createProgram() + await program.parseAsync(buildAddComponentArgs({ extraArgs: ['--route', '/test'] })) + + const output = ctx.consoleOutput.join('\n') + expect(output).toContain(CliErrorCode.GraphNotFound) + }) + + it('returns VALIDATION_ERROR when --type is invalid', async () => { + const program = createProgram() + await program.parseAsync(buildAddComponentArgs({ type: 'InvalidType' })) + + expect(ctx.consoleOutput).toHaveLength(1) + expect(ctx.consoleOutput[0]).toBeTruthy() + + const output: unknown = JSON.parse(assertDefined(ctx.consoleOutput[0], 'Expected output')) + expect(output).toMatchObject({ + success: false, + error: { + code: CliErrorCode.ValidationError, + message: 'Invalid component type: InvalidType', + }, + }) + }) + }) + + describe('adding components', () => { + const ctx: TestContext = createTestContext() + setupCommandTest(ctx) + + it('returns DOMAIN_NOT_FOUND when domain does not exist', async () => { + await createGraphWithDomain(ctx.testDir, 'orders') + const program = createProgram() + await program.parseAsync( + buildAddComponentArgs({ + name: 'Test', + domain: 'nonexistent', + extraArgs: ['--route', '/test'], + }), + ) + expect(ctx.consoleOutput.join('\n')).toContain(CliErrorCode.DomainNotFound) + }) + + it.each([ + { + type: 'UI', + expectedFlag: '--route', + }, + { + type: 'API', + expectedFlag: '--api-type', + }, + { + type: 'DomainOp', + expectedFlag: '--operation-name', + }, + { + type: 'Event', + expectedFlag: '--event-name', + }, + { + type: 'EventHandler', + expectedFlag: '--subscribed-events', + }, + { + type: 'Custom', + expectedFlag: '--custom-type', + }, + ])( + 'returns VALIDATION_ERROR when $type missing $expectedFlag', + async ({ type, expectedFlag }) => { + await createGraphWithDomain(ctx.testDir, 'orders') + const program = createProgram() + await program.parseAsync( + buildAddComponentArgs({ + type, + name: 'Test', + }), + ) + const output = ctx.consoleOutput.join('\n') + expect(output).toContain(CliErrorCode.ValidationError) + expect(output).toContain(expectedFlag) + }, + ) + + it.each([ + { + type: 'UI', + name: 'Checkout Page', + module: 'checkout', + filePath: 'src/pages/checkout.tsx', + extraArgs: ['--route', '/checkout'], + expectedId: 'orders:checkout:ui:checkout-page', + expectedFields: { + type: 'UI', + route: '/checkout', + }, + }, + { + type: 'API', + name: 'Create Order', + module: 'api', + filePath: 'src/api/orders.ts', + extraArgs: ['--api-type', 'REST', '--http-method', 'POST', '--http-path', '/api/orders'], + expectedId: 'orders:api:api:create-order', + expectedFields: { + type: 'API', + apiType: 'REST', + httpMethod: 'POST', + path: '/api/orders', + }, + }, + { + type: 'API', + name: 'List Orders', + module: 'api', + filePath: 'src/api/orders.ts', + extraArgs: ['--api-type', 'REST'], + expectedId: 'orders:api:api:list-orders', + expectedFields: { + type: 'API', + apiType: 'REST', + }, + }, + { + type: 'UseCase', + name: 'Place Order', + module: 'core', + filePath: 'src/usecases/place-order.ts', + extraArgs: [], + expectedId: 'orders:core:usecase:place-order', + expectedFields: { type: 'UseCase' }, + }, + { + type: 'DomainOp', + name: 'Order Create', + module: 'domain', + filePath: 'src/domain/order.ts', + extraArgs: ['--operation-name', 'create', '--entity', 'Order'], + expectedId: 'orders:domain:domainop:order-create', + expectedFields: { + type: 'DomainOp', + operationName: 'create', + entity: 'Order', + }, + }, + { + type: 'DomainOp', + name: 'Order Archive', + module: 'domain', + filePath: 'src/domain/order.ts', + extraArgs: ['--operation-name', 'archive'], + expectedId: 'orders:domain:domainop:order-archive', + expectedFields: { + type: 'DomainOp', + operationName: 'archive', + }, + }, + { + type: 'Event', + name: 'Order Placed', + module: 'events', + filePath: 'src/events/order-placed.ts', + extraArgs: ['--event-name', 'OrderPlaced'], + expectedId: 'orders:events:event:order-placed', + expectedFields: { + type: 'Event', + eventName: 'OrderPlaced', + }, + }, + { + type: 'Event', + name: 'Payment Received', + module: 'events', + filePath: 'src/events/payment-received.ts', + extraArgs: ['--event-name', 'PaymentReceived', '--event-schema', '{ orderId: string }'], + expectedId: 'orders:events:event:payment-received', + expectedFields: { + type: 'Event', + eventName: 'PaymentReceived', + eventSchema: '{ orderId: string }', + }, + }, + { + type: 'EventHandler', + name: 'Send Confirmation', + module: 'handlers', + filePath: 'src/handlers/send-confirmation.ts', + extraArgs: ['--subscribed-events', 'OrderPlaced,PaymentReceived'], + expectedId: 'orders:handlers:eventhandler:send-confirmation', + expectedFields: { + type: 'EventHandler', + subscribedEvents: ['OrderPlaced', 'PaymentReceived'], + }, + }, + ])( + 'creates $type component', + async ({ type, name, module, filePath, extraArgs, expectedId, expectedFields }) => { + await createGraphWithDomain(ctx.testDir, 'orders') + + const program = createProgram() + await program.parseAsync( + buildAddComponentArgs({ + type, + name, + module, + filePath, + extraArgs, + }), + ) + + const graphPath = join(ctx.testDir, '.riviere', 'graph.json') + const content = await readFile(graphPath, 'utf-8') + const graph: unknown = JSON.parse(content) + + expect(graph).toMatchObject({ + components: [ + { + id: expectedId, + ...expectedFields, + }, + ], + }) + }, + ) + + it('returns CUSTOM_TYPE_NOT_FOUND when custom type not defined', async () => { + await createGraphWithDomain(ctx.testDir, 'orders') + + const program = createProgram() + await program.parseAsync( + buildAddComponentArgs({ + type: 'Custom', + name: 'Order Queue', + module: 'messaging', + filePath: 'src/queues/orders.ts', + extraArgs: ['--custom-type', 'MessageQueue'], + }), + ) + + const output = ctx.consoleOutput.join('\n') + expect(output).toContain(CliErrorCode.CustomTypeNotFound) + }) + + it('returns DUPLICATE_COMPONENT when component already exists', async () => { + await createGraphWithDomain(ctx.testDir, 'orders') + + const args = buildAddComponentArgs({ + name: 'Checkout Page', + filePath: 'src/pages/checkout.tsx', + extraArgs: ['--route', '/checkout'], + }) + + const program1 = createProgram() + await program1.parseAsync(args) + + const program2 = createProgram() + await program2.parseAsync(args) + + const output = ctx.consoleOutput.join('\n') + expect(output).toContain(CliErrorCode.DuplicateComponent) + }) + + it('outputs success JSON with component ID when --json flag provided', async () => { + await createGraphWithDomain(ctx.testDir, 'orders') + + const program = createProgram() + await program.parseAsync( + buildAddComponentArgs({ + name: 'Checkout Page', + filePath: 'src/pages/checkout.tsx', + extraArgs: ['--route', '/checkout', '--json'], + }), + ) + + expect(ctx.consoleOutput).toHaveLength(1) + expect(ctx.consoleOutput[0]).toBeTruthy() + + const output: unknown = JSON.parse(assertDefined(ctx.consoleOutput[0], 'Expected output')) + expect(output).toMatchObject({ + success: true, + data: { componentId: 'orders:checkout:ui:checkout-page' }, + }) + }) + + it('includes description when --description provided', async () => { + await createGraphWithDomain(ctx.testDir, 'orders') + const program = createProgram() + await program.parseAsync( + buildAddComponentArgs({ + name: 'Checkout', + module: 'web', + filePath: 'src/checkout.tsx', + extraArgs: ['--route', '/checkout', '--description', 'Main checkout page'], + }), + ) + const graphPath = join(ctx.testDir, '.riviere', 'graph.json') + const content = await readFile(graphPath, 'utf-8') + const graph: unknown = JSON.parse(content) + expect(graph).toMatchObject({ components: [{ description: 'Main checkout page' }] }) + }) + + it('includes lineNumber in sourceLocation when --line-number provided', async () => { + await createGraphWithDomain(ctx.testDir, 'orders') + const program = createProgram() + await program.parseAsync( + buildAddComponentArgs({ + name: 'Checkout', + module: 'web', + filePath: 'src/checkout.tsx', + extraArgs: ['--route', '/checkout', '--line-number', '42'], + }), + ) + const graphPath = join(ctx.testDir, '.riviere', 'graph.json') + const content = await readFile(graphPath, 'utf-8') + const graph: unknown = JSON.parse(content) + expect(graph).toMatchObject({ components: [{ sourceLocation: { lineNumber: 42 } }] }) + }) + + it('includes columnNumber in sourceLocation when --column-number provided', async () => { + await createGraphWithDomain(ctx.testDir, 'orders') + const program = createProgram() + await program.parseAsync( + buildAddComponentArgs({ extraArgs: ['--route', '/test', '--column-number', '17'] }), + ) + + const graphPath = join(ctx.testDir, '.riviere', 'graph.json') + const content = await readFile(graphPath, 'utf-8') + const graph: unknown = JSON.parse(content) + + expect(graph).toMatchObject({ components: [{ sourceLocation: { columnNumber: 17 } }] }) + }) + + it('rejects a fractional --column-number', async () => { + await createGraphWithDomain(ctx.testDir, 'orders') + const program = createProgram() + await program.parseAsync( + buildAddComponentArgs({ extraArgs: ['--route', '/test', '--column-number', '3.14'] }), + ) + + expect(ctx.consoleOutput.join('\n')).toContain( + 'Invalid column number: must be a positive integer', + ) + }) + }) + + describe('getErrorMessage', () => { + it('returns message from Error instance', () => + expect(getErrorMessage(new MockError('test error'))).toBe('test error')) + it('returns Unknown error when input is string', () => + expect(getErrorMessage('string error')).toBe('Unknown error')) + it('returns Unknown error when input is null', () => + expect(getErrorMessage(null)).toBe('Unknown error')) + it('returns Unknown error when input is undefined', () => + expect(getErrorMessage(undefined)).toBe('Unknown error')) + it('returns Unknown error when input is number', () => + expect(getErrorMessage(42)).toBe('Unknown error')) + }) +}) diff --git a/apps/cli/src/features/builder/entrypoint/add-component/entrypoint.ts b/apps/cli/src/features/builder/entrypoint/add-component/entrypoint.ts new file mode 100644 index 000000000..5ca389b0f --- /dev/null +++ b/apps/cli/src/features/builder/entrypoint/add-component/entrypoint.ts @@ -0,0 +1,94 @@ +import { Command } from 'commander' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' +import { getDefaultGraphPathDescription } from '../../../../infra/cli/presentation/graph-path-option' +import { formatError, formatSuccess } from '../../../../infra/cli/presentation/output' +import { getAddComponentHints } from '../../../../infra/cli/presentation/add-component-hints' +import { toAddComponentInput } from './add-component-options' +import type { AddComponent } from '@living-architecture/riviere-builder-use-cases/features/builder/commands/add-component' +import type { AddComponentErrorCode } from '@living-architecture/riviere-builder-use-cases/features/builder/commands/add-component-result' + +interface CliOptions { + type: string + name: string + domain: string + module: string + repository: string + filePath: string + route?: string + apiType?: string + httpMethod?: string + httpPath?: string + operationName?: string + entity?: string + eventName?: string + eventSchema?: string + subscribedEvents?: string + customType?: string + customProperty?: string[] + description?: string + lineNumber?: string + columnNumber?: string + graph?: string + json?: boolean +} + +/** @riviere-role cli-entrypoint */ +export function createAddComponentCommand(addComponent: AddComponent): Command { + return new Command('add-component') + .description('Add a component to the graph') + .requiredOption( + '--type ', + 'Component type (UI, API, UseCase, DomainOp, Event, EventHandler, Custom)', + ) + .requiredOption('--name ', 'Component name') + .requiredOption('--domain ', 'Domain name') + .requiredOption('--module ', 'Module name') + .requiredOption('--repository ', 'Source repository URL') + .requiredOption('--file-path ', 'Source file path') + .option('--route ', 'UI route path') + .option('--api-type ', 'API type (REST, GraphQL, other)') + .option('--http-method ', 'HTTP method') + .option('--http-path ', 'HTTP endpoint path') + .option('--operation-name ', 'Operation name (DomainOp)') + .option('--entity ', 'Entity name (DomainOp)') + .option('--event-name ', 'Event name') + .option('--event-schema ', 'Event schema definition') + .option('--subscribed-events ', 'Comma-separated subscribed event names') + .option('--custom-type ', 'Custom type name') + .option( + '--custom-property ', + 'Custom property (repeatable)', + (val, acc: string[]) => [...acc, val], + [], + ) + .option('--description ', 'Component description') + .option('--line-number ', 'Source line number') + .option('--column-number ', 'Source column number') + .option('--graph ', getDefaultGraphPathDescription()) + .option('--json', 'Output result as JSON') + .action(async (options: CliOptions) => { + const result = addComponent.execute(toAddComponentInput(options)) + + if (!result.success) { + const cliErrorCode = CLI_ERROR_CODES[result.code] + console.log( + JSON.stringify( + formatError(cliErrorCode, result.message, getAddComponentHints(cliErrorCode)), + ), + ) + return + } + + if (options.json) { + console.log(JSON.stringify(formatSuccess({ componentId: result.componentId }))) + } + }) +} + +const CLI_ERROR_CODES: Record = { + VALIDATION_ERROR: CliErrorCode.ValidationError, + GRAPH_NOT_FOUND: CliErrorCode.GraphNotFound, + DOMAIN_NOT_FOUND: CliErrorCode.DomainNotFound, + CUSTOM_TYPE_NOT_FOUND: CliErrorCode.CustomTypeNotFound, + DUPLICATE_COMPONENT: CliErrorCode.DuplicateComponent, +} diff --git a/packages/riviere-cli/src/features/builder/entrypoint/add-domain/add-domain.spec.ts b/apps/cli/src/features/builder/entrypoint/add-domain/add-domain.spec.ts similarity index 76% rename from packages/riviere-cli/src/features/builder/entrypoint/add-domain/add-domain.spec.ts rename to apps/cli/src/features/builder/entrypoint/add-domain/add-domain.spec.ts index c51439afa..659103e5f 100644 --- a/packages/riviere-cli/src/features/builder/entrypoint/add-domain/add-domain.spec.ts +++ b/apps/cli/src/features/builder/entrypoint/add-domain/add-domain.spec.ts @@ -1,20 +1,14 @@ -import { - describe, it, expect, vi, beforeEach, afterEach -} from 'vitest' -import { - readFile, mkdir, writeFile, mkdtemp, rm -} from 'node:fs/promises' -import { tmpdir } from 'node:os' +import { describe, it, expect } from 'vitest' +import { readFile, mkdir, writeFile } from 'node:fs/promises' import { join } from 'node:path' import { createProgram } from '../../../../shell/cli' -import { CliErrorCode } from '../../../../platform/infra/cli/presentation/error-codes' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' import { type TestContext, createTestContext, setupCommandTest, createGraphWithDomain, - MockError, -} from '../../../../platform/__fixtures__/command-test-fixtures' +} from '../../../../__fixtures__/command-test-fixtures' describe('riviere builder add-domain', () => { describe('command registration', () => { @@ -259,63 +253,4 @@ describe('riviere builder add-domain', () => { }) }) }) - - describe('unexpected builder errors', () => { - const mockContext: { - testDir: string - originalCwd: string - } = { - testDir: '', - originalCwd: '', - } - - beforeEach(async () => { - mockContext.testDir = await mkdtemp(join(tmpdir(), 'riviere-test-')) - mockContext.originalCwd = process.cwd() - process.chdir(mockContext.testDir) - vi.resetModules() - }) - - afterEach(async () => { - vi.restoreAllMocks() - process.chdir(mockContext.originalCwd) - await rm(mockContext.testDir, { recursive: true }) - }) - - it('rethrows unexpected errors from builder', async () => { - await createGraphWithDomain(mockContext.testDir, 'orders') - - const unexpectedError = new MockError('Unexpected database error') - const throwUnexpectedError = () => { - throw unexpectedError - } - - vi.doMock('@living-architecture/riviere-builder', () => ({ - RiviereBuilder: { - resume: vi - .fn() - .mockReturnValue({ addDomain: vi.fn().mockImplementation(throwUnexpectedError) }), - }, - DuplicateDomainError: class DuplicateDomainError extends Error {}, - })) - - const { createProgram } = await import('../../../../shell/cli') - const program = createProgram() - - await expect( - program.parseAsync([ - 'node', - 'riviere', - 'builder', - 'add-domain', - '--name', - 'payments', - '--description', - 'Payment processing', - '--system-type', - 'domain', - ]), - ).rejects.toThrow('Unexpected database error') - }) - }) }) diff --git a/apps/cli/src/features/builder/entrypoint/add-domain/entrypoint.ts b/apps/cli/src/features/builder/entrypoint/add-domain/entrypoint.ts new file mode 100644 index 000000000..1b51160d2 --- /dev/null +++ b/apps/cli/src/features/builder/entrypoint/add-domain/entrypoint.ts @@ -0,0 +1,66 @@ +import { Command } from 'commander' +import { formatError, formatSuccess } from '../../../../infra/cli/presentation/output' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' +import { getDefaultGraphPathDescription } from '../../../../infra/cli/presentation/graph-path-option' +import type { AddDomain } from '@living-architecture/riviere-builder-use-cases/features/builder/commands/add-domain' + +interface AddDomainOptions { + name: string + description: string + systemType: string + graph?: string + json?: boolean +} + +/** @riviere-role cli-entrypoint */ +export function createAddDomainCommand(addDomain: AddDomain): Command { + return new Command('add-domain') + .description('Add a domain to the graph') + .addHelpText( + 'after', + ` +Examples: + $ riviere builder add-domain --name orders --system-type domain \\ + --description "Order management" + + $ riviere builder add-domain --name checkout-bff --system-type bff \\ + --description "Checkout backend-for-frontend" +`, + ) + .requiredOption('--name ', 'Domain name') + .requiredOption('--description ', 'Domain description') + .requiredOption( + '--system-type ', + 'System type (domain, bff, ui, external-service, other)', + ) + .option('--graph ', getDefaultGraphPathDescription()) + .option('--json', 'Output result as JSON') + .action(async (options: AddDomainOptions) => { + const result = addDomain.execute({ + description: options.description, + graphPathOption: options.graph, + name: options.name, + systemType: options.systemType, + }) + if (!result.success) { + const errorCodeByResult = { + DUPLICATE_DOMAIN: CliErrorCode.DuplicateDomain, + GRAPH_NOT_FOUND: CliErrorCode.GraphNotFound, + GRAPH_CORRUPTED: CliErrorCode.GraphCorrupted, + VALIDATION_ERROR: CliErrorCode.ValidationError, + } as const + const errorCode = errorCodeByResult[result.code] + const suggestions: string[] = [] + if (result.code === 'DUPLICATE_DOMAIN') { + suggestions.push('Use a different domain name') + } + + console.log(JSON.stringify(formatError(errorCode, result.message, suggestions))) + return + } + + if (options.json === true) { + console.log(JSON.stringify(formatSuccess(result))) + } + }) +} diff --git a/packages/riviere-cli/src/features/builder/entrypoint/add-source/add-source.spec.ts b/apps/cli/src/features/builder/entrypoint/add-source/add-source.spec.ts similarity index 93% rename from packages/riviere-cli/src/features/builder/entrypoint/add-source/add-source.spec.ts rename to apps/cli/src/features/builder/entrypoint/add-source/add-source.spec.ts index cc545e4be..6e7a472d9 100644 --- a/packages/riviere-cli/src/features/builder/entrypoint/add-source/add-source.spec.ts +++ b/apps/cli/src/features/builder/entrypoint/add-source/add-source.spec.ts @@ -1,18 +1,14 @@ -import { - describe, it, expect -} from 'vitest' -import { - readFile, mkdir, writeFile -} from 'node:fs/promises' +import { describe, it, expect } from 'vitest' +import { readFile, mkdir, writeFile } from 'node:fs/promises' import { join } from 'node:path' import { createProgram } from '../../../../shell/cli' -import { CliErrorCode } from '../../../../platform/infra/cli/presentation/error-codes' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' import { type TestContext, createTestContext, setupCommandTest, createGraphWithSource, -} from '../../../../platform/__fixtures__/command-test-fixtures' +} from '../../../../__fixtures__/command-test-fixtures' describe('riviere builder add-source', () => { describe('command registration', () => { diff --git a/apps/cli/src/features/builder/entrypoint/add-source/entrypoint.ts b/apps/cli/src/features/builder/entrypoint/add-source/entrypoint.ts new file mode 100644 index 000000000..2c1522e64 --- /dev/null +++ b/apps/cli/src/features/builder/entrypoint/add-source/entrypoint.ts @@ -0,0 +1,52 @@ +import { Command } from 'commander' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' +import { getDefaultGraphPathDescription } from '../../../../infra/cli/presentation/graph-path-option' +import { formatError, formatSuccess } from '../../../../infra/cli/presentation/output' +import type { AddSource } from '@living-architecture/riviere-builder-use-cases/features/builder/commands/add-source' + +interface AddSourceOptions { + repository: string + graph?: string + json?: boolean +} + +/** @riviere-role cli-entrypoint */ +export function createAddSourceCommand(addSource: AddSource): Command { + return new Command('add-source') + .description('Add a source repository to the graph') + .addHelpText( + 'after', + ` +Examples: + $ riviere builder add-source --repository https://github.com/your-org/orders-service + $ riviere builder add-source --repository https://github.com/your-org/payments-api --json +`, + ) + .requiredOption('--repository ', 'Source repository URL') + .option('--graph ', getDefaultGraphPathDescription()) + .option('--json', 'Output result as JSON') + .action(async (options: AddSourceOptions) => { + const result = addSource.execute({ + graphPathOption: options.graph, + repository: options.repository, + }) + if (!result.success) { + console.log( + JSON.stringify( + formatError( + result.code === 'GRAPH_NOT_FOUND' + ? CliErrorCode.GraphNotFound + : CliErrorCode.GraphCorrupted, + result.message, + [], + ), + ), + ) + return + } + + if (options.json === true) { + console.log(JSON.stringify(formatSuccess({ repository: result.repository }))) + } + }) +} diff --git a/packages/riviere-cli/src/features/builder/entrypoint/add-source/graph-error-coverage.spec.ts b/apps/cli/src/features/builder/entrypoint/add-source/graph-error-coverage.spec.ts similarity index 89% rename from packages/riviere-cli/src/features/builder/entrypoint/add-source/graph-error-coverage.spec.ts rename to apps/cli/src/features/builder/entrypoint/add-source/graph-error-coverage.spec.ts index 220c6de51..8e4a2c2c3 100644 --- a/packages/riviere-cli/src/features/builder/entrypoint/add-source/graph-error-coverage.spec.ts +++ b/apps/cli/src/features/builder/entrypoint/add-source/graph-error-coverage.spec.ts @@ -1,17 +1,13 @@ -import { - mkdir, writeFile -} from 'node:fs/promises' +import { mkdir, writeFile } from 'node:fs/promises' import { join } from 'node:path' -import { - describe, expect, it -} from 'vitest' +import { describe, expect, it } from 'vitest' import { createProgram } from '../../../../shell/cli' -import { CliErrorCode } from '../../../../platform/infra/cli/presentation/error-codes' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' import { type TestContext, createTestContext, setupCommandTest, -} from '../../../../platform/__fixtures__/command-test-fixtures' +} from '../../../../__fixtures__/command-test-fixtures' async function createInvalidGraph(testDir: string): Promise { const graphDir = join(testDir, '.riviere') diff --git a/packages/riviere-cli/src/features/builder/entrypoint/check-consistency/check-consistency.spec.ts b/apps/cli/src/features/builder/entrypoint/check-consistency/check-consistency.spec.ts similarity index 96% rename from packages/riviere-cli/src/features/builder/entrypoint/check-consistency/check-consistency.spec.ts rename to apps/cli/src/features/builder/entrypoint/check-consistency/check-consistency.spec.ts index c22bcb06c..f5c2c2719 100644 --- a/packages/riviere-cli/src/features/builder/entrypoint/check-consistency/check-consistency.spec.ts +++ b/apps/cli/src/features/builder/entrypoint/check-consistency/check-consistency.spec.ts @@ -1,9 +1,7 @@ -import { - describe, it, expect -} from 'vitest' +import { describe, it, expect } from 'vitest' import { createProgram } from '../../../../shell/cli' -import { CliErrorCode } from '../../../../platform/infra/cli/presentation/error-codes' -import type { TestContext } from '../../../../platform/__fixtures__/command-test-fixtures' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' +import type { TestContext } from '../../../../__fixtures__/command-test-fixtures' import { createTestContext, setupCommandTest, @@ -15,7 +13,7 @@ import { hasSuccessOutputStructure, testCommandRegistration, testCustomGraphPath, -} from '../../../../platform/__fixtures__/command-test-fixtures' +} from '../../../../__fixtures__/command-test-fixtures' interface ConsistencyWarning { code: string diff --git a/apps/cli/src/features/builder/entrypoint/check-consistency/entrypoint.ts b/apps/cli/src/features/builder/entrypoint/check-consistency/entrypoint.ts new file mode 100644 index 000000000..0d7de25c0 --- /dev/null +++ b/apps/cli/src/features/builder/entrypoint/check-consistency/entrypoint.ts @@ -0,0 +1,54 @@ +import { Command } from 'commander' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' +import { getDefaultGraphPathDescription } from '../../../../infra/cli/presentation/graph-path-option' +import { formatError, formatSuccess } from '../../../../infra/cli/presentation/output' +import type { CheckConsistency } from '@living-architecture/riviere-builder-use-cases/features/builder/commands/check-consistency' + +interface CheckConsistencyOptions { + graph?: string + json?: boolean +} + +/** @riviere-role cli-entrypoint */ +export function createCheckConsistencyCommand(checkConsistency: CheckConsistency): Command { + return new Command('check-consistency') + .description('Check for structural issues in the graph') + .addHelpText( + 'after', + ` +Examples: + $ riviere builder check-consistency + $ riviere builder check-consistency --json +`, + ) + .option('--graph ', getDefaultGraphPathDescription()) + .option('--json', 'Output result as JSON') + .action(async (options: CheckConsistencyOptions) => { + const result = checkConsistency.execute({ graphPathOption: options.graph }) + if (!result.success) { + console.log( + JSON.stringify( + formatError( + result.code === 'GRAPH_NOT_FOUND' + ? CliErrorCode.GraphNotFound + : CliErrorCode.GraphCorrupted, + result.message, + [], + ), + ), + ) + return + } + + if (options.json === true) { + console.log( + JSON.stringify( + formatSuccess({ + consistent: result.consistent, + warnings: result.warnings, + }), + ), + ) + } + }) +} diff --git a/packages/riviere-cli/src/features/builder/entrypoint/component-checklist/component-checklist.spec.ts b/apps/cli/src/features/builder/entrypoint/component-checklist/component-checklist.spec.ts similarity index 96% rename from packages/riviere-cli/src/features/builder/entrypoint/component-checklist/component-checklist.spec.ts rename to apps/cli/src/features/builder/entrypoint/component-checklist/component-checklist.spec.ts index f676a79ff..b59ea6d7c 100644 --- a/packages/riviere-cli/src/features/builder/entrypoint/component-checklist/component-checklist.spec.ts +++ b/apps/cli/src/features/builder/entrypoint/component-checklist/component-checklist.spec.ts @@ -1,9 +1,7 @@ -import { - describe, it, expect -} from 'vitest' +import { describe, it, expect } from 'vitest' import { createProgram } from '../../../../shell/cli' -import { CliErrorCode } from '../../../../platform/infra/cli/presentation/error-codes' -import type { TestContext } from '../../../../platform/__fixtures__/command-test-fixtures' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' +import type { TestContext } from '../../../../__fixtures__/command-test-fixtures' import { createTestContext, setupCommandTest, @@ -15,7 +13,7 @@ import { hasSuccessOutputStructure, testCommandRegistration, testCustomGraphPath, -} from '../../../../platform/__fixtures__/command-test-fixtures' +} from '../../../../__fixtures__/command-test-fixtures' interface ChecklistComponent { id: string diff --git a/apps/cli/src/features/builder/entrypoint/component-checklist/entrypoint.ts b/apps/cli/src/features/builder/entrypoint/component-checklist/entrypoint.ts new file mode 100644 index 000000000..586ed0c7c --- /dev/null +++ b/apps/cli/src/features/builder/entrypoint/component-checklist/entrypoint.ts @@ -0,0 +1,62 @@ +import { Command } from 'commander' +import { getDefaultGraphPathDescription } from '../../../../infra/cli/presentation/graph-path-option' +import { formatError, formatSuccess } from '../../../../infra/cli/presentation/output' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' +import type { ComponentChecklist } from '@living-architecture/riviere-builder-use-cases/features/builder/commands/component-checklist' + +interface ComponentChecklistOptions { + graph?: string + json?: boolean + type?: string +} + +/** @riviere-role cli-entrypoint */ +export function createComponentChecklistCommand(componentChecklist: ComponentChecklist): Command { + return new Command('component-checklist') + .description('List components as a checklist for linking/enrichment') + .addHelpText( + 'after', + ` +Examples: + $ riviere builder component-checklist + $ riviere builder component-checklist --type DomainOp + $ riviere builder component-checklist --type API --json +`, + ) + .option('--graph ', getDefaultGraphPathDescription()) + .option('--json', 'Output result as JSON') + .option('--type ', 'Filter by component type') + .action(async (options: ComponentChecklistOptions) => { + const result = componentChecklist.execute({ + graphPathOption: options.graph, + type: options.type, + }) + if (!result.success) { + console.log( + JSON.stringify( + formatError( + { + GRAPH_CORRUPTED: CliErrorCode.GraphCorrupted, + GRAPH_NOT_FOUND: CliErrorCode.GraphNotFound, + VALIDATION_ERROR: CliErrorCode.InvalidComponentType, + }[result.code], + result.message, + [], + ), + ), + ) + return + } + + if (options.json === true) { + console.log( + JSON.stringify( + formatSuccess({ + components: result.components, + total: result.total, + }), + ), + ) + } + }) +} diff --git a/packages/riviere-cli/src/features/builder/entrypoint/component-summary/component-summary.spec.ts b/apps/cli/src/features/builder/entrypoint/component-summary/component-summary.spec.ts similarity index 94% rename from packages/riviere-cli/src/features/builder/entrypoint/component-summary/component-summary.spec.ts rename to apps/cli/src/features/builder/entrypoint/component-summary/component-summary.spec.ts index 006f9c8ff..3e61f73f2 100644 --- a/packages/riviere-cli/src/features/builder/entrypoint/component-summary/component-summary.spec.ts +++ b/apps/cli/src/features/builder/entrypoint/component-summary/component-summary.spec.ts @@ -1,9 +1,7 @@ -import { - describe, it, expect -} from 'vitest' +import { describe, it, expect } from 'vitest' import { createProgram } from '../../../../shell/cli' -import { CliErrorCode } from '../../../../platform/infra/cli/presentation/error-codes' -import type { TestContext } from '../../../../platform/__fixtures__/command-test-fixtures' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' +import type { TestContext } from '../../../../__fixtures__/command-test-fixtures' import { createTestContext, setupCommandTest, @@ -14,7 +12,7 @@ import { parseSuccessOutput, hasSuccessOutputStructure, testCommandRegistration, -} from '../../../../platform/__fixtures__/command-test-fixtures' +} from '../../../../__fixtures__/command-test-fixtures' interface ComponentSummaryOutput { success: true diff --git a/apps/cli/src/features/builder/entrypoint/component-summary/entrypoint.ts b/apps/cli/src/features/builder/entrypoint/component-summary/entrypoint.ts new file mode 100644 index 000000000..b0393eddd --- /dev/null +++ b/apps/cli/src/features/builder/entrypoint/component-summary/entrypoint.ts @@ -0,0 +1,43 @@ +import { Command } from 'commander' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' +import { getDefaultGraphPathDescription } from '../../../../infra/cli/presentation/graph-path-option' +import { formatError, formatSuccess } from '../../../../infra/cli/presentation/output' +import type { ComponentSummary } from '@living-architecture/riviere-builder-use-cases/features/builder/commands/component-summary' + +interface ComponentSummaryOptions { + graph?: string +} + +/** @riviere-role cli-entrypoint */ +export function createComponentSummaryCommand(componentSummary: ComponentSummary): Command { + return new Command('component-summary') + .description('Show component counts by type and domain') + .addHelpText( + 'after', + ` +Examples: + $ riviere builder component-summary + $ riviere builder component-summary > summary.json +`, + ) + .option('--graph ', getDefaultGraphPathDescription()) + .action(async (options: ComponentSummaryOptions) => { + const result = componentSummary.execute({ graphPathOption: options.graph }) + if (!result.success) { + console.log( + JSON.stringify( + formatError( + result.code === 'GRAPH_NOT_FOUND' + ? CliErrorCode.GraphNotFound + : CliErrorCode.GraphCorrupted, + result.message, + [], + ), + ), + ) + return + } + + console.log(JSON.stringify(formatSuccess(result))) + }) +} diff --git a/apps/cli/src/features/builder/entrypoint/define-custom-type/custom-type-parser.ts b/apps/cli/src/features/builder/entrypoint/define-custom-type/custom-type-parser.ts new file mode 100644 index 000000000..f0af14998 --- /dev/null +++ b/apps/cli/src/features/builder/entrypoint/define-custom-type/custom-type-parser.ts @@ -0,0 +1,64 @@ +interface ParsedPropertyDefinition { + description?: string + type: string +} + +function parsePropertySpec(spec: string): + | { + definition: ParsedPropertyDefinition + name: string + } + | { error: string } { + const parts = spec.split(':') + if (parts.length < 2 || parts.length > 3) + return { + error: `Invalid property format: "${spec}". Expected "name:type" or "name:type:description"`, + } + const [name, type, description] = parts + if (!name || name.trim() === '') return { error: 'Property name cannot be empty' } + if (!type || type.trim() === '') return { error: 'Property type cannot be empty' } + const definition: ParsedPropertyDefinition = { type: type.trim() } + if (description && description.trim() !== '') definition.description = description + return { + definition, + name: name.trim(), + } +} + +type ParsePropertiesResult = + | { + properties: Record + success: true + } + | { + error: string + success: false + } + +/** @riviere-role entrypoint-cli-input-parser */ +export function parsePropertySpecs(specs: string[] | undefined): ParsePropertiesResult { + if (specs === undefined || specs.length === 0) + return { + properties: {}, + success: true, + } + const properties: Record = {} + for (const spec of specs) { + const result = parsePropertySpec(spec) + if ('error' in result) + return { + error: result.error, + success: false, + } + if (properties[result.name] !== undefined) + return { + error: `Duplicate property name: "${result.name}"`, + success: false, + } + properties[result.name] = result.definition + } + return { + properties, + success: true, + } +} diff --git a/packages/riviere-cli/src/features/builder/entrypoint/define-custom-type/define-custom-type.spec.ts b/apps/cli/src/features/builder/entrypoint/define-custom-type/define-custom-type.spec.ts similarity index 90% rename from packages/riviere-cli/src/features/builder/entrypoint/define-custom-type/define-custom-type.spec.ts rename to apps/cli/src/features/builder/entrypoint/define-custom-type/define-custom-type.spec.ts index fdf66bc61..2a257d804 100644 --- a/packages/riviere-cli/src/features/builder/entrypoint/define-custom-type/define-custom-type.spec.ts +++ b/apps/cli/src/features/builder/entrypoint/define-custom-type/define-custom-type.spec.ts @@ -1,16 +1,14 @@ -import { - describe, it, expect -} from 'vitest' +import { describe, it, expect } from 'vitest' import { readFile } from 'node:fs/promises' import { join } from 'node:path' import { createProgram } from '../../../../shell/cli' -import { CliErrorCode } from '../../../../platform/infra/cli/presentation/error-codes' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' import { type TestContext, createTestContext, setupCommandTest, createGraphWithDomain, -} from '../../../../platform/__fixtures__/command-test-fixtures' +} from '../../../../__fixtures__/command-test-fixtures' describe('riviere builder define-custom-type', () => { describe('command registration', () => { @@ -68,7 +66,9 @@ describe('riviere builder define-custom-type', () => { const content = await readFile(graphPath, 'utf-8') const graph: unknown = JSON.parse(content) - expect(graph).toMatchObject({metadata: { customTypes: { MessageQueue: { description: 'Async message queue' } } },}) + expect(graph).toMatchObject({ + metadata: { customTypes: { MessageQueue: { description: 'Async message queue' } } }, + }) }) it('stores required properties when provided', async () => { @@ -258,6 +258,26 @@ describe('riviere builder define-custom-type', () => { expect(output).toContain('Property name cannot be empty') }) + it('returns VALIDATION_ERROR for empty property type', async () => { + await createGraphWithDomain(ctx.testDir, 'orders') + + const program = createProgram() + await program.parseAsync([ + 'node', + 'riviere', + 'builder', + 'define-custom-type', + '--name', + 'MessageQueue', + '--required-property', + 'queueName:', + ]) + + const output = ctx.consoleOutput.join('\n') + expect(output).toContain(CliErrorCode.ValidationError) + expect(output).toContain('Property type cannot be empty') + }) + it('returns VALIDATION_ERROR for duplicate property names', async () => { await createGraphWithDomain(ctx.testDir, 'orders') diff --git a/apps/cli/src/features/builder/entrypoint/define-custom-type/entrypoint.ts b/apps/cli/src/features/builder/entrypoint/define-custom-type/entrypoint.ts new file mode 100644 index 000000000..5a041a598 --- /dev/null +++ b/apps/cli/src/features/builder/entrypoint/define-custom-type/entrypoint.ts @@ -0,0 +1,78 @@ +import { Command } from 'commander' +import { getDefaultGraphPathDescription } from '../../../../infra/cli/presentation/graph-path-option' +import { formatError, formatSuccess } from '../../../../infra/cli/presentation/output' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' +import { parsePropertySpecs } from './custom-type-parser' +import { collectOption } from '../_platform/cli/option-collectors' +import type { DefineCustomType } from '@living-architecture/riviere-builder-use-cases/features/builder/commands/define-custom-type' + +interface DefineCustomTypeOptions { + name: string + description?: string + requiredProperty?: string[] + optionalProperty?: string[] + graph?: string + json?: boolean +} + +/** @riviere-role cli-entrypoint */ +export function createDefineCustomTypeCommand(defineCustomType: DefineCustomType): Command { + return new Command('define-custom-type') + .description('Define a custom component type') + .requiredOption('--name ', 'Custom type name') + .option('--description ', 'Custom type description') + .option( + '--required-property ', + 'Required property (format: name:type[:description])', + collectOption, + [], + ) + .option( + '--optional-property ', + 'Optional property (format: name:type[:description])', + collectOption, + [], + ) + .option('--graph ', getDefaultGraphPathDescription()) + .option('--json', 'Output result as JSON') + .action(async (options: DefineCustomTypeOptions) => { + const requiredResult = parsePropertySpecs(options.requiredProperty) + if (!requiredResult.success) { + console.log( + JSON.stringify(formatError(CliErrorCode.ValidationError, requiredResult.error, [])), + ) + return + } + + const optionalResult = parsePropertySpecs(options.optionalProperty) + if (!optionalResult.success) { + console.log( + JSON.stringify(formatError(CliErrorCode.ValidationError, optionalResult.error, [])), + ) + return + } + + const result = defineCustomType.execute({ + description: options.description, + graphPathOption: options.graph, + name: options.name, + optionalProperties: optionalResult.properties, + requiredProperties: requiredResult.properties, + }) + if (!result.success) { + const errorCodeByResult = { + GRAPH_CORRUPTED: CliErrorCode.GraphCorrupted, + GRAPH_NOT_FOUND: CliErrorCode.GraphNotFound, + VALIDATION_ERROR: CliErrorCode.ValidationError, + } as const + const errorCode = errorCodeByResult[result.code] + + console.log(JSON.stringify(formatError(errorCode, result.message, []))) + return + } + + if (options.json === true) { + console.log(JSON.stringify(formatSuccess(result))) + } + }) +} diff --git a/packages/riviere-cli/src/features/builder/entrypoint/define-relationship-type/define-relationship-type.spec.ts b/apps/cli/src/features/builder/entrypoint/define-relationship-type/define-relationship-type.spec.ts similarity index 86% rename from packages/riviere-cli/src/features/builder/entrypoint/define-relationship-type/define-relationship-type.spec.ts rename to apps/cli/src/features/builder/entrypoint/define-relationship-type/define-relationship-type.spec.ts index a39de28ea..52e03ba0a 100644 --- a/packages/riviere-cli/src/features/builder/entrypoint/define-relationship-type/define-relationship-type.spec.ts +++ b/apps/cli/src/features/builder/entrypoint/define-relationship-type/define-relationship-type.spec.ts @@ -1,18 +1,24 @@ -import { - describe, expect, it -} from 'vitest' +import { describe, expect, it } from 'vitest' import { readFile } from 'node:fs/promises' import { join } from 'node:path' import { createProgram } from '../../../../shell/cli' -import { CliErrorCode } from '../../../../platform/infra/cli/presentation/error-codes' -import { parseRiviereGraph } from '@living-architecture/riviere-schema' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' +import { parseRiviereGraph } from '@living-architecture/riviere-schema-published-language/validation' import { type TestContext, assertDefined, createGraphWithDomain, createTestContext, setupCommandTest, -} from '../../../../platform/__fixtures__/command-test-fixtures' +} from '../../../../__fixtures__/command-test-fixtures' + +function parseValidGraph(value: unknown) { + const result = parseRiviereGraph(value) + if (!result.success) { + expect.fail(result.issues.join('\n')) + } + return result.graph +} describe('riviere builder define-relationship-type', () => { const ctx: TestContext = createTestContext() @@ -40,7 +46,7 @@ describe('riviere builder define-relationship-type', () => { 'Invokes the target during execution', ]) - const graph = parseRiviereGraph( + const graph = parseValidGraph( JSON.parse(await readFile(join(ctx.testDir, '.riviere', 'graph.json'), 'utf-8')), ) expect(graph.metadata.relationshipTypes?.executes?.description).toBe( diff --git a/apps/cli/src/features/builder/entrypoint/define-relationship-type/entrypoint.ts b/apps/cli/src/features/builder/entrypoint/define-relationship-type/entrypoint.ts new file mode 100644 index 000000000..9d33dbe9f --- /dev/null +++ b/apps/cli/src/features/builder/entrypoint/define-relationship-type/entrypoint.ts @@ -0,0 +1,44 @@ +import { Command } from 'commander' +import { getDefaultGraphPathDescription } from '../../../../infra/cli/presentation/graph-path-option' +import { formatError, formatSuccess } from '../../../../infra/cli/presentation/output' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' +import type { DefineRelationshipType } from '@living-architecture/riviere-builder-use-cases/features/builder/commands/define-relationship-type' + +interface DefineRelationshipTypeOptions { + name: string + description: string + graph?: string + json?: boolean +} + +/** @riviere-role cli-entrypoint */ +export function createDefineRelationshipTypeCommand( + defineRelationshipType: DefineRelationshipType, +): Command { + return new Command('define-relationship-type') + .description('Define a project relationship type') + .requiredOption('--name ', 'Relationship type name') + .requiredOption('--description ', 'Relationship type description') + .option('--graph ', getDefaultGraphPathDescription()) + .option('--json', 'Output result as JSON') + .action(async (options: DefineRelationshipTypeOptions) => { + const result = defineRelationshipType.execute({ + description: options.description, + graphPathOption: options.graph, + name: options.name, + }) + if (!result.success) { + const errorCodeByResult = { + GRAPH_CORRUPTED: CliErrorCode.GraphCorrupted, + GRAPH_NOT_FOUND: CliErrorCode.GraphNotFound, + VALIDATION_ERROR: CliErrorCode.ValidationError, + } as const + console.log(JSON.stringify(formatError(errorCodeByResult[result.code], result.message, []))) + return + } + + if (options.json === true) { + console.log(JSON.stringify(formatSuccess(result))) + } + }) +} diff --git a/packages/riviere-cli/src/features/builder/entrypoint/enrich/enrich.signature.spec.ts b/apps/cli/src/features/builder/entrypoint/enrich/enrich.signature.spec.ts similarity index 91% rename from packages/riviere-cli/src/features/builder/entrypoint/enrich/enrich.signature.spec.ts rename to apps/cli/src/features/builder/entrypoint/enrich/enrich.signature.spec.ts index c7a8d0245..29af6b431 100644 --- a/packages/riviere-cli/src/features/builder/entrypoint/enrich/enrich.signature.spec.ts +++ b/apps/cli/src/features/builder/entrypoint/enrich/enrich.signature.spec.ts @@ -1,17 +1,15 @@ -import { - describe, it, expect -} from 'vitest' +import { describe, it, expect } from 'vitest' import { readFile } from 'node:fs/promises' import { join } from 'node:path' import { createProgram } from '../../../../shell/cli' -import { CliErrorCode } from '../../../../platform/infra/cli/presentation/error-codes' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' import { type TestContext, createTestContext, setupCommandTest, createGraphWithComponent, domainOpComponent, -} from '../../../../platform/__fixtures__/command-test-fixtures' +} from '../../../../__fixtures__/command-test-fixtures' describe('riviere builder enrich - signature option', () => { const ctx: TestContext = createTestContext() @@ -66,9 +64,7 @@ describe('riviere builder enrich - signature option', () => { input: '-> Order', expected: { returnType: 'Order' }, }, - ])('parses $name', async ({ - input, expected - }) => { + ])('parses $name', async ({ input, expected }) => { await createGraphWithComponent(ctx.testDir, domainOpComponent) const program = createProgram() await program.parseAsync([ diff --git a/packages/riviere-cli/src/features/builder/entrypoint/enrich/enrich.spec.ts b/apps/cli/src/features/builder/entrypoint/enrich/enrich.spec.ts similarity index 97% rename from packages/riviere-cli/src/features/builder/entrypoint/enrich/enrich.spec.ts rename to apps/cli/src/features/builder/entrypoint/enrich/enrich.spec.ts index f1afc293d..81842feee 100644 --- a/packages/riviere-cli/src/features/builder/entrypoint/enrich/enrich.spec.ts +++ b/apps/cli/src/features/builder/entrypoint/enrich/enrich.spec.ts @@ -1,10 +1,8 @@ -import { - describe, it, expect -} from 'vitest' +import { describe, it, expect } from 'vitest' import { readFile } from 'node:fs/promises' import { join } from 'node:path' import { createProgram } from '../../../../shell/cli' -import { CliErrorCode } from '../../../../platform/infra/cli/presentation/error-codes' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' import { type TestContext, createTestContext, @@ -12,7 +10,7 @@ import { createGraphWithComponent, domainOpComponent, simpleUseCaseComponent, -} from '../../../../platform/__fixtures__/command-test-fixtures' +} from '../../../../__fixtures__/command-test-fixtures' describe('riviere builder enrich', () => { describe('command registration', () => { @@ -345,9 +343,7 @@ describe('riviere builder enrich', () => { emits: ['order-placed event'], }, }, - ])('enriches DomainOp with $name', async ({ - args, expected - }) => { + ])('enriches DomainOp with $name', async ({ args, expected }) => { await createGraphWithComponent(ctx.testDir, domainOpComponent) const program = createProgram() await program.parseAsync([ diff --git a/apps/cli/src/features/builder/entrypoint/enrich/enrichment-parser.ts b/apps/cli/src/features/builder/entrypoint/enrich/enrichment-parser.ts new file mode 100644 index 000000000..d2b4e065f --- /dev/null +++ b/apps/cli/src/features/builder/entrypoint/enrich/enrichment-parser.ts @@ -0,0 +1,41 @@ +interface ParsedStateTransition { + from: string + to: string +} + +function parseStateChange(input: string): ParsedStateTransition | undefined { + const [from, to, ...rest] = input.split(':') + if (from === undefined || to === undefined || rest.length > 0) return undefined + return { + from, + to, + } +} + +type ParseResult = + | { + stateChanges: ParsedStateTransition[] + success: true + } + | { + invalidInput: string + success: false + } + +/** @riviere-role entrypoint-cli-input-parser */ +export function parseStateChanges(inputs: string[]): ParseResult { + const stateChanges: ParsedStateTransition[] = [] + for (const sc of inputs) { + const parsed = parseStateChange(sc) + if (parsed === undefined) + return { + invalidInput: sc, + success: false, + } + stateChanges.push(parsed) + } + return { + stateChanges, + success: true, + } +} diff --git a/apps/cli/src/features/builder/entrypoint/enrich/entrypoint.ts b/apps/cli/src/features/builder/entrypoint/enrich/entrypoint.ts new file mode 100644 index 000000000..da3651927 --- /dev/null +++ b/apps/cli/src/features/builder/entrypoint/enrich/entrypoint.ts @@ -0,0 +1,115 @@ +import { Command } from 'commander' +import { formatError, formatSuccess } from '../../../../infra/cli/presentation/output' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' +import { getDefaultGraphPathDescription } from '../../../../infra/cli/presentation/graph-path-option' +import { collectOption } from '../_platform/cli/option-collectors' +import { parseStateChanges } from './enrichment-parser' +import { parseSignature } from './signature-parser' +import type { EnrichComponent } from '@living-architecture/riviere-builder-use-cases/features/builder/commands/enrich-component' + +interface EnrichOptions { + id: string + entity?: string + stateChange: string[] + businessRule: string[] + reads: string[] + validates: string[] + modifies: string[] + emits: string[] + signature?: string + graph?: string + json?: boolean +} + +/** @riviere-role cli-entrypoint */ +export function createEnrichCommand(enrichComponent: EnrichComponent): Command { + return new Command('enrich') + .description( + 'Enrich a DomainOp component with semantic information. ' + + 'Note: Enrichment is additive — running multiple times accumulates values.', + ) + .addHelpText( + 'after', + ` +Examples: + $ riviere builder enrich \\ + --id "orders:checkout:domainop:orderbegin" \\ + --entity Order \\ + --state-change "Draft:Placed" \\ + --business-rule "Order must have at least one item" \\ + --reads "this.items" \\ + --validates "items.length > 0" \\ + --modifies "this.state <- Placed" \\ + --emits "OrderPlaced event" + + $ riviere builder enrich \\ + --id "payments:gateway:domainop:paymentprocess" \\ + --state-change "Pending:Processing" \\ + --reads "amount parameter" \\ + --validates "amount > 0" \\ + --modifies "this.status <- Processing" +`, + ) + .requiredOption('--id ', 'Component ID to enrich') + .option('--entity ', 'Entity name') + .option('--state-change ', 'State transition (repeatable)', collectOption, []) + .option('--business-rule ', 'Business rule (repeatable)', collectOption, []) + .option('--reads ', 'What the operation reads (repeatable)', collectOption, []) + .option('--validates ', 'What the operation validates (repeatable)', collectOption, []) + .option('--modifies ', 'What the operation modifies (repeatable)', collectOption, []) + .option('--emits ', 'What the operation emits (repeatable)', collectOption, []) + .option( + '--signature ', + 'Operation signature (e.g., "orderId:string, amount:number -> Order")', + ) + .option('--graph ', getDefaultGraphPathDescription()) + .option('--json', 'Output result as JSON') + .action(async (options: EnrichOptions) => { + const parseResult = parseStateChanges(options.stateChange) + if (!parseResult.success) { + const msg = `Invalid state-change format: '${parseResult.invalidInput}'. Expected 'from:to'.` + console.log(JSON.stringify(formatError(CliErrorCode.ValidationError, msg, []))) + return + } + + const signatureResult = + options.signature === undefined ? undefined : parseSignature(options.signature) + if (signatureResult !== undefined && !signatureResult.success) { + console.log( + JSON.stringify(formatError(CliErrorCode.ValidationError, signatureResult.error, [])), + ) + return + } + const parsedSignature = + signatureResult?.success === true ? signatureResult.signature : undefined + + const result = enrichComponent.execute({ + businessRules: options.businessRule, + entity: options.entity, + emits: options.emits, + graphPathOption: options.graph, + id: options.id, + modifies: options.modifies, + reads: options.reads, + signature: parsedSignature, + stateChanges: parseResult.stateChanges, + validates: options.validates, + }) + if (!result.success) { + const errorCodeByResult = { + COMPONENT_NOT_FOUND: CliErrorCode.ComponentNotFound, + GRAPH_CORRUPTED: CliErrorCode.GraphCorrupted, + GRAPH_NOT_FOUND: CliErrorCode.GraphNotFound, + INVALID_COMPONENT_TYPE: CliErrorCode.InvalidComponentType, + } as const + const errorCode = errorCodeByResult[result.code] + + console.log(JSON.stringify(formatError(errorCode, result.message, result.suggestions))) + return + } + + if (options.json === true) { + console.log(JSON.stringify(formatSuccess({ componentId: result.componentId }))) + } + }) +} diff --git a/apps/cli/src/features/builder/entrypoint/enrich/signature-parser.ts b/apps/cli/src/features/builder/entrypoint/enrich/signature-parser.ts new file mode 100644 index 000000000..302fc6f18 --- /dev/null +++ b/apps/cli/src/features/builder/entrypoint/enrich/signature-parser.ts @@ -0,0 +1,107 @@ +interface ParsedOperationParameter { + description?: string + name: string + type: string +} + +interface ParsedOperationSignature { + parameters?: ParsedOperationParameter[] + returnType?: string +} + +function parseParameter(input: string): ParsedOperationParameter | undefined { + const parts = input.split(':') + if (parts.length < 2 || parts.length > 3) return undefined + const [name, type, description] = parts + if (name === undefined || name === '' || type === undefined || type === '') return undefined + return { + ...(description !== undefined && description !== '' ? { description: description.trim() } : {}), + name: name.trim(), + type: type.trim(), + } +} + +type SignatureParseResult = + | { + signature: ParsedOperationSignature + success: true + } + | { + error: string + success: false + } +type ParametersParseResult = + | { + parameters: ParsedOperationParameter[] + success: true + } + | { + error: string + success: false + } + +function parseParameters(paramsPart: string): ParametersParseResult { + if (paramsPart === '') + return { + parameters: [], + success: true, + } + const paramStrings = paramsPart.split(',').map((p) => p.trim()) + const parameters: ParsedOperationParameter[] = [] + for (const paramStr of paramStrings) { + const param = parseParameter(paramStr) + if (param === undefined) + return { + error: `Invalid parameter format: '${paramStr}'. Expected 'name:type' or 'name:type:description'.`, + success: false, + } + parameters.push(param) + } + return { + parameters, + success: true, + } +} + +function buildSignatureObject( + parameters: ParsedOperationParameter[], + returnType: string | undefined, +): ParsedOperationSignature { + const signature: ParsedOperationSignature = {} + if (parameters.length > 0) signature.parameters = parameters + if (returnType !== undefined && returnType !== '') signature.returnType = returnType + return signature +} + +/** @riviere-role entrypoint-cli-input-parser */ +export function parseSignature(input: string): SignatureParseResult { + const trimmed = input.trim() + if (trimmed.startsWith('->')) { + const returnType = trimmed.slice(2).trim() + return returnType === '' + ? { + error: `Invalid signature format: '${input}'. Return type cannot be empty.`, + success: false, + } + : { + signature: { returnType }, + success: true, + } + } + const arrowIndex = trimmed.indexOf(' -> ') + const paramsPart = arrowIndex === -1 ? trimmed : trimmed.slice(0, arrowIndex).trim() + const returnType = arrowIndex === -1 ? undefined : trimmed.slice(arrowIndex + 4).trim() + const paramsResult = parseParameters(paramsPart) + if (!paramsResult.success) return paramsResult + const signature = buildSignatureObject(paramsResult.parameters, returnType) + if (paramsResult.parameters.length === 0 && returnType === undefined) { + return { + error: `Invalid signature format: '${input}'. Expected 'param:type, ... -> ReturnType' or '-> ReturnType' or 'param:type'.`, + success: false, + } + } + return { + signature, + success: true, + } +} diff --git a/apps/cli/src/features/builder/entrypoint/finalize/entrypoint.ts b/apps/cli/src/features/builder/entrypoint/finalize/entrypoint.ts new file mode 100644 index 000000000..72f1f42fe --- /dev/null +++ b/apps/cli/src/features/builder/entrypoint/finalize/entrypoint.ts @@ -0,0 +1,53 @@ +import { Command } from 'commander' +import { writeFile } from 'node:fs/promises' +import { formatError, formatSuccess } from '../../../../infra/cli/presentation/output' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' +import { getDefaultGraphPathDescription } from '../../../../infra/cli/presentation/graph-path-option' +import type { FinalizeGraph } from '@living-architecture/riviere-builder-use-cases/features/builder/commands/finalize-graph' + +interface FinalizeOptions { + graph?: string + output?: string + json?: boolean +} + +/** @riviere-role cli-entrypoint */ +export function createFinalizeCommand(finalizeGraph: FinalizeGraph): Command { + return new Command('finalize') + .description('Validate and export the final graph') + .addHelpText( + 'after', + ` +Examples: + $ riviere builder finalize + $ riviere builder finalize --output ./dist/architecture.json + $ riviere builder finalize --json +`, + ) + .option('--graph ', getDefaultGraphPathDescription()) + .option('--output ', 'Output path for finalized graph (defaults to input path)') + .option('--json', 'Output result as JSON') + .action(async (options: FinalizeOptions) => { + const result = finalizeGraph.execute({ graphPathOption: options.graph }) + if (!result.success) { + const errorCodeByResult = { + GRAPH_CORRUPTED: CliErrorCode.GraphCorrupted, + GRAPH_NOT_FOUND: CliErrorCode.GraphNotFound, + VALIDATION_ERROR: CliErrorCode.ValidationError, + } as const + const errorCode = errorCodeByResult[result.code] + const suggestions = + result.code === 'VALIDATION_ERROR' ? ['Fix the validation errors and try again'] : [] + + console.log(JSON.stringify(formatError(errorCode, result.message, suggestions))) + return + } + + const outputPath = options.output ?? options.graph ?? '.riviere/graph.json' + await writeFile(outputPath, JSON.stringify(result.finalGraph, null, 2), 'utf-8') + + if (options.json === true) { + console.log(JSON.stringify(formatSuccess({ path: outputPath }))) + } + }) +} diff --git a/packages/riviere-cli/src/features/builder/entrypoint/finalize/finalize.spec.ts b/apps/cli/src/features/builder/entrypoint/finalize/finalize.spec.ts similarity index 95% rename from packages/riviere-cli/src/features/builder/entrypoint/finalize/finalize.spec.ts rename to apps/cli/src/features/builder/entrypoint/finalize/finalize.spec.ts index 6d81a37f7..59f24666d 100644 --- a/packages/riviere-cli/src/features/builder/entrypoint/finalize/finalize.spec.ts +++ b/apps/cli/src/features/builder/entrypoint/finalize/finalize.spec.ts @@ -1,20 +1,16 @@ -import { - describe, it, expect -} from 'vitest' -import { - mkdir, readFile, access -} from 'node:fs/promises' +import { describe, it, expect } from 'vitest' +import { mkdir, readFile, access } from 'node:fs/promises' import { join } from 'node:path' import { createProgram } from '../../../../shell/cli' -import { CliErrorCode } from '../../../../platform/infra/cli/presentation/error-codes' -import type { TestContext } from '../../../../platform/__fixtures__/command-test-fixtures' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' +import type { TestContext } from '../../../../__fixtures__/command-test-fixtures' import { createTestContext, setupCommandTest, createGraph, baseMetadata, useCaseComponent, -} from '../../../../platform/__fixtures__/command-test-fixtures' +} from '../../../../__fixtures__/command-test-fixtures' interface FinalizeSuccessOutput { success: true diff --git a/packages/riviere-cli/src/features/builder/entrypoint/init/domain-input-parser.ts b/apps/cli/src/features/builder/entrypoint/init/domain-input-parser.ts similarity index 77% rename from packages/riviere-cli/src/features/builder/entrypoint/init/domain-input-parser.ts rename to apps/cli/src/features/builder/entrypoint/init/domain-input-parser.ts index e9cf97723..8a053b5ab 100644 --- a/packages/riviere-cli/src/features/builder/entrypoint/init/domain-input-parser.ts +++ b/apps/cli/src/features/builder/entrypoint/init/domain-input-parser.ts @@ -1,6 +1,3 @@ -import type { SystemType } from '@living-architecture/riviere-schema' -import { isValidSystemType } from '../../../../entrypoint/_platform/cli/component-types' - class InvalidDomainJsonError extends Error { readonly value: string @@ -14,7 +11,7 @@ class InvalidDomainJsonError extends Error { interface DomainInputParsed { description: string name: string - systemType: SystemType + systemType: string } function isDomainInputParsed(value: unknown): value is DomainInputParsed { @@ -25,8 +22,7 @@ function isDomainInputParsed(value: unknown): value is DomainInputParsed { 'description' in value && typeof value.description === 'string' && 'systemType' in value && - typeof value.systemType === 'string' && - isValidSystemType(value.systemType) + typeof value.systemType === 'string' ) } diff --git a/apps/cli/src/features/builder/entrypoint/init/entrypoint.ts b/apps/cli/src/features/builder/entrypoint/init/entrypoint.ts new file mode 100644 index 000000000..dd754add0 --- /dev/null +++ b/apps/cli/src/features/builder/entrypoint/init/entrypoint.ts @@ -0,0 +1,108 @@ +import { Command } from 'commander' +import { formatError, formatSuccess } from '../../../../infra/cli/presentation/output' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' +import { getDefaultGraphPathDescription } from '../../../../infra/cli/presentation/graph-path-option' +import { collectOption } from '../_platform/cli/option-collectors' +import { parseDomainJson } from './domain-input-parser' +import type { InitGraph } from '@living-architecture/riviere-builder-use-cases/features/builder/commands/init-graph' + +interface InitOptions { + name?: string + graph?: string + json?: boolean + source: string[] + domain: DomainInputParsed[] +} + +interface DomainInputParsed { + description: string + name: string + systemType: string +} + +/** @riviere-role cli-entrypoint */ +export function createInitCommand(initGraph: InitGraph): Command { + return new Command('init') + .description('Initialize a new graph') + .addHelpText( + 'after', + ` +Examples: + $ riviere builder init --source https://github.com/your-org/your-repo \\ + --domain '{"name":"orders","description":"Order management","systemType":"domain"}' + + $ riviere builder init --name "ecommerce" \\ + --source https://github.com/your-org/orders \\ + --source https://github.com/your-org/payments \\ + --domain '{"name":"orders","description":"Order management","systemType":"domain"}' \\ + --domain '{"name":"payments","description":"Payment processing","systemType":"domain"}' +`, + ) + .option('--name ', 'System name') + .option('--graph ', getDefaultGraphPathDescription()) + .option('--json', 'Output result as JSON') + .option('--source ', 'Source repository URL (repeatable)', collectOption, []) + .option('--domain ', 'Domain as JSON (repeatable)', parseDomainJson, []) + .action(async (options: InitOptions) => { + // Validate required flags + if (options.source.length === 0) { + console.log( + JSON.stringify( + formatError(CliErrorCode.ValidationError, 'At least one source required', [ + 'Add --source flag', + ]), + ), + ) + return + } + + if (options.domain.length === 0) { + console.log( + JSON.stringify( + formatError(CliErrorCode.ValidationError, 'At least one domain required', [ + 'Add --domain flag', + ]), + ), + ) + return + } + + const domains = options.domain.map(({ description, name, systemType }) => ({ + description, + name, + systemType, + })) + + const result = initGraph.execute({ + domains, + graphPathOption: options.graph, + name: options.name, + sources: options.source, + }) + + if (!result.success) { + console.log( + JSON.stringify( + result.code === 'VALIDATION_ERROR' + ? formatError(CliErrorCode.ValidationError, result.message) + : formatError(CliErrorCode.GraphExists, result.message, [ + 'Delete the file to reinitialize', + ]), + ), + ) + return + } + + if (options.json === true) { + console.log( + JSON.stringify( + formatSuccess({ + domains: result.domains, + path: result.path, + sources: result.sources, + }), + ), + ) + } + }) +} diff --git a/packages/riviere-cli/src/features/builder/entrypoint/init/init-options.spec.ts b/apps/cli/src/features/builder/entrypoint/init/init-options.spec.ts similarity index 96% rename from packages/riviere-cli/src/features/builder/entrypoint/init/init-options.spec.ts rename to apps/cli/src/features/builder/entrypoint/init/init-options.spec.ts index 093ad3b17..2266c2736 100644 --- a/packages/riviere-cli/src/features/builder/entrypoint/init/init-options.spec.ts +++ b/apps/cli/src/features/builder/entrypoint/init/init-options.spec.ts @@ -1,13 +1,9 @@ -import { - describe, it, expect, beforeEach, afterEach, vi -} from 'vitest' -import { - mkdtemp, rm, readFile, stat -} from 'node:fs/promises' +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' +import { mkdtemp, rm, readFile, stat } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { createProgram } from '../../../../shell/cli' -import { CliErrorCode } from '../../../../platform/infra/cli/presentation/error-codes' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' interface InitSuccessOutput { success: true diff --git a/packages/riviere-cli/src/features/builder/entrypoint/init/init.spec.ts b/apps/cli/src/features/builder/entrypoint/init/init.spec.ts similarity index 91% rename from packages/riviere-cli/src/features/builder/entrypoint/init/init.spec.ts rename to apps/cli/src/features/builder/entrypoint/init/init.spec.ts index c9908002e..27f10c154 100644 --- a/packages/riviere-cli/src/features/builder/entrypoint/init/init.spec.ts +++ b/apps/cli/src/features/builder/entrypoint/init/init.spec.ts @@ -1,17 +1,13 @@ -import { - describe, it, expect -} from 'vitest' -import { - readFile, stat, mkdir, writeFile -} from 'node:fs/promises' +import { describe, it, expect } from 'vitest' +import { readFile, stat, mkdir, writeFile } from 'node:fs/promises' import { join } from 'node:path' import { createProgram } from '../../../../shell/cli' -import { CliErrorCode } from '../../../../platform/infra/cli/presentation/error-codes' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' import { type TestContext, createTestContext, setupCommandTest, -} from '../../../../platform/__fixtures__/command-test-fixtures' +} from '../../../../__fixtures__/command-test-fixtures' describe('riviere builder init', () => { describe('command registration', () => { @@ -70,7 +66,9 @@ describe('riviere builder init', () => { const content = await readFile(graphPath, 'utf-8') const graph: unknown = JSON.parse(content) - expect(graph).toMatchObject({metadata: { sources: [{ repository: 'https://github.com/org/repo' }] },}) + expect(graph).toMatchObject({ + metadata: { sources: [{ repository: 'https://github.com/org/repo' }] }, + }) }) it('includes multiple sources when multiple --source flags provided', async () => { @@ -299,6 +297,24 @@ describe('riviere builder init', () => { const ctx: TestContext = createTestContext() setupCommandTest(ctx) + it('returns VALIDATION_ERROR when a domain has an unsupported system type', async () => { + const program = createProgram() + + await program.parseAsync([ + 'node', + 'riviere', + 'builder', + 'init', + '--json', + '--source', + 'https://github.com/org/repo', + '--domain', + '{"name":"orders","description":"Order management","systemType":"unsupported"}', + ]) + + expect(ctx.consoleOutput.join('\n')).toContain(CliErrorCode.ValidationError) + }) + it('throws when domain JSON is not valid JSON', async () => { const program = createProgram() diff --git a/apps/cli/src/features/builder/entrypoint/link-external/entrypoint.ts b/apps/cli/src/features/builder/entrypoint/link-external/entrypoint.ts new file mode 100644 index 000000000..908bc6ed7 --- /dev/null +++ b/apps/cli/src/features/builder/entrypoint/link-external/entrypoint.ts @@ -0,0 +1,71 @@ +import { Command } from 'commander' +import { getDefaultGraphPathDescription } from '../../../../infra/cli/presentation/graph-path-option' +import { formatError, formatSuccess } from '../../../../infra/cli/presentation/output' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' +import type { LinkExternal } from '@living-architecture/riviere-builder-use-cases/features/builder/commands/link-external' + +interface LinkExternalOptions { + from: string + targetName: string + targetDomain?: string + targetUrl?: string + linkType?: string + graph?: string + json?: boolean +} + +/** @riviere-role cli-entrypoint */ +export function createLinkExternalCommand(linkExternal: LinkExternal): Command { + return new Command('link-external') + .description('Link a component to an external system') + .addHelpText( + 'after', + ` +Examples: + $ riviere builder link-external \\ + --from "payments:gateway:usecase:processpayment" \\ + --target-name "Stripe" \\ + --target-url "https://api.stripe.com" \\ + --link-type sync + + $ riviere builder link-external \\ + --from "shipping:tracking:usecase:updatetracking" \\ + --target-name "FedEx API" \\ + --target-domain "shipping" \\ + --link-type async +`, + ) + .requiredOption('--from ', 'Source component ID') + .requiredOption('--target-name ', 'External target name') + .option('--target-domain ', 'External target domain') + .option('--target-url ', 'External target URL') + .option('--link-type ', 'Link type (sync, async)') + .option('--graph ', getDefaultGraphPathDescription()) + .option('--json', 'Output result as JSON') + .action(async (options: LinkExternalOptions) => { + const result = linkExternal.execute({ + from: options.from, + graphPathOption: options.graph, + targetDomain: options.targetDomain, + targetName: options.targetName, + targetUrl: options.targetUrl, + type: options.linkType, + }) + if (!result.success) { + const errorCodeByResult = { + COMPONENT_NOT_FOUND: CliErrorCode.ComponentNotFound, + GRAPH_CORRUPTED: CliErrorCode.GraphCorrupted, + GRAPH_NOT_FOUND: CliErrorCode.GraphNotFound, + VALIDATION_ERROR: CliErrorCode.ValidationError, + } as const + const errorCode = errorCodeByResult[result.code] + + console.log(JSON.stringify(formatError(errorCode, result.message, result.suggestions))) + return + } + + if (options.json) { + console.log(JSON.stringify(formatSuccess({ externalLink: result.externalLink }))) + } + }) +} diff --git a/packages/riviere-cli/src/features/builder/entrypoint/link-external/link-external.spec.ts b/apps/cli/src/features/builder/entrypoint/link-external/link-external.spec.ts similarity index 87% rename from packages/riviere-cli/src/features/builder/entrypoint/link-external/link-external.spec.ts rename to apps/cli/src/features/builder/entrypoint/link-external/link-external.spec.ts index 4edfdf310..41331e680 100644 --- a/packages/riviere-cli/src/features/builder/entrypoint/link-external/link-external.spec.ts +++ b/apps/cli/src/features/builder/entrypoint/link-external/link-external.spec.ts @@ -1,17 +1,10 @@ -import { - describe, it, expect -} from 'vitest' -import { - mkdir, writeFile, readFile -} from 'node:fs/promises' +import { describe, it, expect } from 'vitest' +import { mkdir, writeFile, readFile } from 'node:fs/promises' import { join } from 'node:path' import { createProgram } from '../../../../shell/cli' -import { CliErrorCode } from '../../../../platform/infra/cli/presentation/error-codes' -import type { TestContext } from '../../../../platform/__fixtures__/command-test-fixtures' -import { - createTestContext, - setupCommandTest, -} from '../../../../platform/__fixtures__/command-test-fixtures' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' +import type { TestContext } from '../../../../__fixtures__/command-test-fixtures' +import { createTestContext, setupCommandTest } from '../../../../__fixtures__/command-test-fixtures' describe('riviere builder link-external', () => { describe('command registration', () => { @@ -238,23 +231,32 @@ describe('riviere builder link-external', () => { }) }) - it('propagates error when source ID format is malformed', async () => { + it('returns VALIDATION_ERROR when source ID format is malformed', async () => { await createGraphWithComponent() const program = createProgram() - await expect( - program.parseAsync([ - 'node', - 'riviere', - 'builder', - 'link-external', - '--from', - 'malformed-id', - '--target-name', - 'Stripe API', - ]), - ).rejects.toThrow(/Invalid component ID format/) + await program.parseAsync([ + 'node', + 'riviere', + 'builder', + 'link-external', + '--from', + 'malformed-id', + '--target-name', + 'Stripe API', + ]) + + expect(ctx.consoleOutput[0]).toBeTruthy() + const output: unknown = JSON.parse(ctx.consoleOutput[0]) + expect(output).toMatchObject({ + success: false, + error: { + code: CliErrorCode.ValidationError, + message: + "Invalid component ID format: 'malformed-id'. Expected 'domain:module:type:name'", + }, + }) }) it('returns VALIDATION_ERROR when link type is invalid', async () => { diff --git a/apps/cli/src/features/builder/entrypoint/link-http/entrypoint.ts b/apps/cli/src/features/builder/entrypoint/link-http/entrypoint.ts new file mode 100644 index 000000000..f63583ead --- /dev/null +++ b/apps/cli/src/features/builder/entrypoint/link-http/entrypoint.ts @@ -0,0 +1,75 @@ +import { Command } from 'commander' +import { getDefaultGraphPathDescription } from '../../../../infra/cli/presentation/graph-path-option' +import { formatError, formatSuccess } from '../../../../infra/cli/presentation/output' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' +import type { LinkHttp } from '@living-architecture/riviere-builder-use-cases/features/builder/commands/link-http' + +interface LinkHttpOptions { + path: string + toDomain: string + toModule: string + toType: string + toName: string + method?: string + linkType?: string + graph?: string + json?: boolean +} + +/** @riviere-role cli-entrypoint */ +export function createLinkHttpCommand(linkHttp: LinkHttp): Command { + return new Command('link-http') + .description('Find an API by HTTP path and link to a target component') + .addHelpText( + 'after', + ` +Examples: + $ riviere builder link-http \\ + --path "/orders" --method POST \\ + --to-domain orders --to-module checkout --to-type UseCase --to-name "place-order" + + $ riviere builder link-http \\ + --path "/users/{id}" --method GET \\ + --to-domain users --to-module queries --to-type UseCase --to-name "get-user" \\ + --link-type sync +`, + ) + .requiredOption('--path ', 'HTTP path to match') + .requiredOption('--to-domain ', 'Target domain') + .requiredOption('--to-module ', 'Target module') + .requiredOption('--to-type ', 'Target component type') + .requiredOption('--to-name ', 'Target component name') + .option('--method ', 'Filter by HTTP method (GET, POST, PUT, PATCH, DELETE)') + .option('--link-type ', 'Link type (sync, async)') + .option('--graph ', getDefaultGraphPathDescription()) + .option('--json', 'Output result as JSON') + .action(async (options: LinkHttpOptions) => { + const result = linkHttp.execute({ + graphPathOption: options.graph, + httpMethod: options.method, + linkType: options.linkType, + path: options.path, + targetDomain: options.toDomain, + targetModule: options.toModule, + targetName: options.toName, + targetType: options.toType, + }) + if (!result.success) { + const errorCodeByResult = { + AMBIGUOUS_API_MATCH: CliErrorCode.AmbiguousApiMatch, + COMPONENT_NOT_FOUND: CliErrorCode.ComponentNotFound, + GRAPH_CORRUPTED: CliErrorCode.GraphCorrupted, + GRAPH_NOT_FOUND: CliErrorCode.GraphNotFound, + VALIDATION_ERROR: CliErrorCode.ValidationError, + } as const + const errorCode = errorCodeByResult[result.code] + + console.log(JSON.stringify(formatError(errorCode, result.message, result.suggestions))) + return + } + + if (options.json) { + console.log(JSON.stringify(formatSuccess(result))) + } + }) +} diff --git a/packages/riviere-cli/src/features/builder/entrypoint/link-http/link-http.spec.ts b/apps/cli/src/features/builder/entrypoint/link-http/link-http.spec.ts similarity index 94% rename from packages/riviere-cli/src/features/builder/entrypoint/link-http/link-http.spec.ts rename to apps/cli/src/features/builder/entrypoint/link-http/link-http.spec.ts index 6515904bf..49c0d71d2 100644 --- a/packages/riviere-cli/src/features/builder/entrypoint/link-http/link-http.spec.ts +++ b/apps/cli/src/features/builder/entrypoint/link-http/link-http.spec.ts @@ -1,17 +1,10 @@ -import { - describe, it, expect -} from 'vitest' -import { - mkdir, writeFile, readFile -} from 'node:fs/promises' +import { describe, it, expect } from 'vitest' +import { mkdir, writeFile, readFile } from 'node:fs/promises' import { join } from 'node:path' import { createProgram } from '../../../../shell/cli' -import { CliErrorCode } from '../../../../platform/infra/cli/presentation/error-codes' -import type { TestContext } from '../../../../platform/__fixtures__/command-test-fixtures' -import { - createTestContext, - setupCommandTest, -} from '../../../../platform/__fixtures__/command-test-fixtures' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' +import type { TestContext } from '../../../../__fixtures__/command-test-fixtures' +import { createTestContext, setupCommandTest } from '../../../../__fixtures__/command-test-fixtures' interface ApiComponentDef { id: string @@ -252,9 +245,7 @@ describe('riviere builder link-http', () => { override: { method: 'INVALID' }, message: 'Invalid HTTP method: INVALID', }, - ])('returns VALIDATION_ERROR for invalid input: $message', async ({ - override, message - }) => { + ])('returns VALIDATION_ERROR for invalid input: $message', async ({ override, message }) => { await createGraph([singleApi]) await createProgram().parseAsync(buildArgs(override)) expect(ctx.consoleOutput[0]).toBeTruthy() diff --git a/apps/cli/src/features/builder/entrypoint/link/entrypoint.ts b/apps/cli/src/features/builder/entrypoint/link/entrypoint.ts new file mode 100644 index 000000000..4b687fb02 --- /dev/null +++ b/apps/cli/src/features/builder/entrypoint/link/entrypoint.ts @@ -0,0 +1,107 @@ +import { Command } from 'commander' +import { getDefaultGraphPathDescription } from '../../../../infra/cli/presentation/graph-path-option' +import * as cliOutput from '../../../../infra/cli/presentation/output' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' +import type { LinkComponents } from '@living-architecture/riviere-builder-use-cases/features/builder/commands/link-components' +import { parseLinkSourceLocation } from './link-source-location-options' + +interface LinkOptions { + from: string + toDomain: string + toModule: string + toType: string + toName: string + linkType?: string + relationshipType?: string + condition?: string + repository?: string + filePath?: string + lineNumber?: string + columnNumber?: string + graph?: string + json?: boolean +} + +/** @riviere-role cli-entrypoint */ +export function createLinkCommand(linkComponents: LinkComponents): Command { + return new Command('link') + .description('Link two components') + .addHelpText( + 'after', + ` +Examples: + $ riviere builder link \\ + --from "orders:api:api:postorders" \\ + --to-domain orders --to-module checkout --to-type UseCase --to-name "place-order" \\ + --link-type sync + + $ riviere builder link \\ + --from "orders:checkout:domainop:orderbegin" \\ + --to-domain orders --to-module events --to-type Event --to-name "order-placed" \\ + --link-type async +`, + ) + .requiredOption('--from ', 'Source component ID') + .requiredOption('--to-domain ', 'Target domain') + .requiredOption('--to-module ', 'Target module') + .requiredOption( + '--to-type ', + 'Target component type (UI, API, UseCase, DomainOp, Event, EventHandler, Custom)', + ) + .requiredOption('--to-name ', 'Target component name') + .option('--link-type ', 'Link type (sync, async)') + .option('--relationship-type ', 'Project-defined relationship type') + .option('--condition ', 'Condition retained exactly as supplied') + .option('--repository ', 'Source repository identifier') + .option('--file-path ', 'Source file path') + .option('--line-number ', 'Source line number') + .option('--column-number ', 'Source column number') + .option('--graph ', getDefaultGraphPathDescription()) + .option('--json', 'Output result as JSON') + .action(async (options: LinkOptions) => { + const sourceLocationResult = parseLinkSourceLocation(options) + if (!sourceLocationResult.success) { + console.log( + JSON.stringify( + cliOutput.formatError(CliErrorCode.ValidationError, sourceLocationResult.message, []), + ), + ) + return + } + + const result = linkComponents.execute({ + from: options.from, + graphPathOption: options.graph, + targetDomain: options.toDomain, + targetModule: options.toModule, + targetName: options.toName, + targetType: options.toType, + type: options.linkType, + ...(options.condition === undefined ? {} : { condition: options.condition }), + ...(options.relationshipType === undefined + ? {} + : { relationshipType: options.relationshipType }), + ...(sourceLocationResult.sourceLocation === undefined + ? {} + : { sourceLocation: sourceLocationResult.sourceLocation }), + }) + if (!result.success) { + const errorCodeByResult = { + COMPONENT_NOT_FOUND: CliErrorCode.ComponentNotFound, + GRAPH_CORRUPTED: CliErrorCode.GraphCorrupted, + GRAPH_NOT_FOUND: CliErrorCode.GraphNotFound, + VALIDATION_ERROR: CliErrorCode.ValidationError, + } as const + const errorCode = errorCodeByResult[result.code] + + console.log( + JSON.stringify(cliOutput.formatError(errorCode, result.message, result.suggestions)), + ) + return + } + + if (options.json) { + console.log(JSON.stringify(cliOutput.formatSuccess({ link: result.link }))) + } + }) +} diff --git a/packages/riviere-cli/src/features/builder/entrypoint/link/link-relationship.spec.ts b/apps/cli/src/features/builder/entrypoint/link/link-relationship.spec.ts similarity index 90% rename from packages/riviere-cli/src/features/builder/entrypoint/link/link-relationship.spec.ts rename to apps/cli/src/features/builder/entrypoint/link/link-relationship.spec.ts index 273375d1e..a5f566298 100644 --- a/packages/riviere-cli/src/features/builder/entrypoint/link/link-relationship.spec.ts +++ b/apps/cli/src/features/builder/entrypoint/link/link-relationship.spec.ts @@ -1,17 +1,23 @@ -import { - describe, expect, it -} from 'vitest' +import { describe, expect, it } from 'vitest' import { readFile } from 'node:fs/promises' import { join } from 'node:path' -import { parseRiviereGraph } from '@living-architecture/riviere-schema' +import { parseRiviereGraph } from '@living-architecture/riviere-schema-published-language/validation' import { createProgram } from '../../../../shell/cli' -import { CliErrorCode } from '../../../../platform/infra/cli/presentation/error-codes' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' import { type TestContext, createGraphWithComponent, createTestContext, setupCommandTest, -} from '../../../../platform/__fixtures__/command-test-fixtures' +} from '../../../../__fixtures__/command-test-fixtures' + +function parseValidGraph(value: unknown) { + const result = parseRiviereGraph(value) + if (!result.success) { + expect.fail(result.issues.join('\n')) + } + return result.graph +} const sourceComponent = { id: 'orders:checkout:api:create-order', @@ -77,7 +83,7 @@ describe('riviere builder link relationship fields', () => { '5', ]) - const graph = parseRiviereGraph( + const graph = parseValidGraph( JSON.parse(await readFile(join(ctx.testDir, '.riviere', 'graph.json'), 'utf-8')), ) expect(graph.links).toStrictEqual([ @@ -136,7 +142,7 @@ describe('riviere builder link relationship fields', () => { 'src/api/orders.ts', ]) - const graph = parseRiviereGraph( + const graph = parseValidGraph( JSON.parse(await readFile(join(ctx.testDir, '.riviere', 'graph.json'), 'utf-8')), ) expect(graph.links[0].sourceLocation).toStrictEqual({ diff --git a/packages/riviere-cli/src/features/builder/entrypoint/link/link-source-location-options.ts b/apps/cli/src/features/builder/entrypoint/link/link-source-location-options.ts similarity index 83% rename from packages/riviere-cli/src/features/builder/entrypoint/link/link-source-location-options.ts rename to apps/cli/src/features/builder/entrypoint/link/link-source-location-options.ts index 8868af775..7c2589cf0 100644 --- a/packages/riviere-cli/src/features/builder/entrypoint/link/link-source-location-options.ts +++ b/apps/cli/src/features/builder/entrypoint/link/link-source-location-options.ts @@ -1,5 +1,3 @@ -import type { SourceLocation } from '@living-architecture/riviere-schema' - interface LinkSourceLocationOptions { repository?: string filePath?: string @@ -9,13 +7,20 @@ interface LinkSourceLocationOptions { type LinkSourceLocationResult = | { - success: true - sourceLocation: SourceLocation | undefined - } + success: true + sourceLocation: + | { + repository: string + filePath: string + lineNumber?: number + columnNumber?: number + } + | undefined + } | { - success: false - message: string - } + success: false + message: string + } /** @riviere-role entrypoint-cli-input-parser */ export function parseLinkSourceLocation( @@ -64,13 +69,13 @@ function parsePositiveInteger( optionName: string, ): | { - success: true - value: number | undefined - } + success: true + value: number | undefined + } | { - success: false - message: string - } { + success: false + message: string + } { if (raw === undefined) { return { success: true, diff --git a/packages/riviere-cli/src/features/builder/entrypoint/link/link.spec.ts b/apps/cli/src/features/builder/entrypoint/link/link.spec.ts similarity index 89% rename from packages/riviere-cli/src/features/builder/entrypoint/link/link.spec.ts rename to apps/cli/src/features/builder/entrypoint/link/link.spec.ts index e066c9657..48ad4799e 100644 --- a/packages/riviere-cli/src/features/builder/entrypoint/link/link.spec.ts +++ b/apps/cli/src/features/builder/entrypoint/link/link.spec.ts @@ -1,16 +1,14 @@ -import { - describe, it, expect -} from 'vitest' +import { describe, it, expect } from 'vitest' import { readFile } from 'node:fs/promises' import { join } from 'node:path' import { createProgram } from '../../../../shell/cli' -import { CliErrorCode } from '../../../../platform/infra/cli/presentation/error-codes' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' import { type TestContext, createTestContext, setupCommandTest, createGraphWithComponent, -} from '../../../../platform/__fixtures__/command-test-fixtures' +} from '../../../../__fixtures__/command-test-fixtures' describe('riviere builder link', () => { describe('command registration', () => { @@ -245,29 +243,38 @@ describe('riviere builder link', () => { }) }) - it('propagates error when source ID format is malformed', async () => { + it('returns VALIDATION_ERROR when source ID format is malformed', async () => { await createGraphWithComponent(ctx.testDir, linkSourceComponent) const program = createProgram() - await expect( - program.parseAsync([ - 'node', - 'riviere', - 'builder', - 'link', - '--from', - 'malformed-id', - '--to-domain', - 'orders', - '--to-module', - 'checkout', - '--to-type', - 'UseCase', - '--to-name', - 'place-order', - ]), - ).rejects.toThrow(/Invalid component ID format/) + await program.parseAsync([ + 'node', + 'riviere', + 'builder', + 'link', + '--from', + 'malformed-id', + '--to-domain', + 'orders', + '--to-module', + 'checkout', + '--to-type', + 'UseCase', + '--to-name', + 'place-order', + ]) + + expect(ctx.consoleOutput[0]).toBeTruthy() + const output: unknown = JSON.parse(ctx.consoleOutput[0]) + expect(output).toMatchObject({ + success: false, + error: { + code: CliErrorCode.ValidationError, + message: + "Invalid component ID format: 'malformed-id'. Expected 'domain:module:type:name'", + }, + }) }) it('returns VALIDATION_ERROR when component type is invalid', async () => { diff --git a/apps/cli/src/features/builder/entrypoint/validate/entrypoint.ts b/apps/cli/src/features/builder/entrypoint/validate/entrypoint.ts new file mode 100644 index 000000000..3c9fa442a --- /dev/null +++ b/apps/cli/src/features/builder/entrypoint/validate/entrypoint.ts @@ -0,0 +1,56 @@ +import { Command } from 'commander' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' +import { getDefaultGraphPathDescription } from '../../../../infra/cli/presentation/graph-path-option' +import { formatError, formatSuccess } from '../../../../infra/cli/presentation/output' +import type { ValidateGraph } from '@living-architecture/riviere-builder-use-cases/features/builder/commands/validate-graph' + +interface ValidateOptions { + graph?: string + json?: boolean +} + +/** @riviere-role cli-entrypoint */ +export function createValidateCommand(validateGraph: ValidateGraph): Command { + return new Command('validate') + .description('Validate the graph for errors and warnings') + .addHelpText( + 'after', + ` +Examples: + $ riviere builder validate + $ riviere builder validate --json + $ riviere builder validate --graph .riviere/my-graph.json +`, + ) + .option('--graph ', getDefaultGraphPathDescription()) + .option('--json', 'Output result as JSON') + .action(async (options: ValidateOptions) => { + const result = validateGraph.execute({ graphPathOption: options.graph }) + if (!result.success) { + console.log( + JSON.stringify( + formatError( + result.code === 'GRAPH_NOT_FOUND' + ? CliErrorCode.GraphNotFound + : CliErrorCode.GraphCorrupted, + result.message, + [], + ), + ), + ) + return + } + + if (options.json === true) { + console.log( + JSON.stringify( + formatSuccess({ + errors: result.errors, + valid: result.valid, + warnings: result.warnings, + }), + ), + ) + } + }) +} diff --git a/packages/riviere-cli/src/features/builder/entrypoint/validate/validate.spec.ts b/apps/cli/src/features/builder/entrypoint/validate/validate.spec.ts similarity index 96% rename from packages/riviere-cli/src/features/builder/entrypoint/validate/validate.spec.ts rename to apps/cli/src/features/builder/entrypoint/validate/validate.spec.ts index cc8ea98c2..20eb7ba1a 100644 --- a/packages/riviere-cli/src/features/builder/entrypoint/validate/validate.spec.ts +++ b/apps/cli/src/features/builder/entrypoint/validate/validate.spec.ts @@ -1,9 +1,7 @@ -import { - describe, it, expect -} from 'vitest' +import { describe, it, expect } from 'vitest' import { createProgram } from '../../../../shell/cli' -import { CliErrorCode } from '../../../../platform/infra/cli/presentation/error-codes' -import type { TestContext } from '../../../../platform/__fixtures__/command-test-fixtures' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' +import type { TestContext } from '../../../../__fixtures__/command-test-fixtures' import { createTestContext, setupCommandTest, @@ -14,7 +12,7 @@ import { apiComponent, validLink, TestAssertionError, -} from '../../../../platform/__fixtures__/command-test-fixtures' +} from '../../../../__fixtures__/command-test-fixtures' interface ValidationOutput { success: true diff --git a/packages/riviere-cli/src/features/extract/__fixtures__/extraction-test-fixtures.ts b/apps/cli/src/features/extract/__fixtures__/extraction-test-fixtures.ts similarity index 95% rename from packages/riviere-cli/src/features/extract/__fixtures__/extraction-test-fixtures.ts rename to apps/cli/src/features/extract/__fixtures__/extraction-test-fixtures.ts index a2aca8952..32d6d334d 100644 --- a/packages/riviere-cli/src/features/extract/__fixtures__/extraction-test-fixtures.ts +++ b/apps/cli/src/features/extract/__fixtures__/extraction-test-fixtures.ts @@ -1,9 +1,7 @@ -import { - writeFile, mkdir -} from 'node:fs/promises' +import { writeFile, mkdir } from 'node:fs/promises' import { join } from 'node:path' import { z } from 'zod' -import { TestAssertionError } from '../../../platform/__fixtures__/command-test-fixtures' +import { TestAssertionError } from '../../../__fixtures__/command-test-fixtures' const draftComponentSchema = z.looseObject({ type: z.string(), diff --git a/packages/riviere-cli/src/features/extract/entrypoint/extract/categorize-components.spec.ts b/apps/cli/src/features/extract/entrypoint/extract/categorize-components.spec.ts similarity index 97% rename from packages/riviere-cli/src/features/extract/entrypoint/extract/categorize-components.spec.ts rename to apps/cli/src/features/extract/entrypoint/extract/categorize-components.spec.ts index 9259eee92..5cdf77289 100644 --- a/packages/riviere-cli/src/features/extract/entrypoint/extract/categorize-components.spec.ts +++ b/apps/cli/src/features/extract/entrypoint/extract/categorize-components.spec.ts @@ -1,7 +1,5 @@ -import { - describe, it, expect -} from 'vitest' -import type { DraftComponent } from '@living-architecture/riviere-extract-ts' +import { describe, it, expect } from 'vitest' +import type { DraftComponent } from '@living-architecture/riviere-extract-ts-domain-model/domain/component-extraction/draft-component' import { categorizeComponents } from './categorize-components' function createDraftComponent(type: string, name: string, domain: string): DraftComponent { diff --git a/packages/riviere-cli/src/features/extract/entrypoint/extract/categorize-components.ts b/apps/cli/src/features/extract/entrypoint/extract/categorize-components.ts similarity index 83% rename from packages/riviere-cli/src/features/extract/entrypoint/extract/categorize-components.ts rename to apps/cli/src/features/extract/entrypoint/extract/categorize-components.ts index cf4a47113..b9d43b512 100644 --- a/packages/riviere-cli/src/features/extract/entrypoint/extract/categorize-components.ts +++ b/apps/cli/src/features/extract/entrypoint/extract/categorize-components.ts @@ -1,4 +1,9 @@ -import type { DraftComponent } from '@living-architecture/riviere-extract-ts' +import type { ExtractDraftComponentsResult } from '@living-architecture/riviere-extract-ts-use-cases/features/extract/commands/extract-draft-components-result' + +type DraftComponent = Extract< + ExtractDraftComponentsResult, + { kind: 'draftOnly' } +>['components'][number] interface ComponentIdentity { readonly type: string diff --git a/packages/riviere-cli/src/features/extract/entrypoint/extract/create-command-inputs.spec.ts b/apps/cli/src/features/extract/entrypoint/extract/create-command-inputs.spec.ts similarity index 96% rename from packages/riviere-cli/src/features/extract/entrypoint/extract/create-command-inputs.spec.ts rename to apps/cli/src/features/extract/entrypoint/extract/create-command-inputs.spec.ts index 7725abbff..e8e04e0f1 100644 --- a/packages/riviere-cli/src/features/extract/entrypoint/extract/create-command-inputs.spec.ts +++ b/apps/cli/src/features/extract/entrypoint/extract/create-command-inputs.spec.ts @@ -1,6 +1,4 @@ -import { - describe, expect, it -} from 'vitest' +import { describe, expect, it } from 'vitest' import { createEnrichDraftComponentsInput } from './create-enrich-draft-components-input' import { createExtractDraftComponentsInput } from './create-extract-draft-components-input' diff --git a/packages/riviere-cli/src/features/extract/entrypoint/extract/create-enrich-draft-components-input.ts b/apps/cli/src/features/extract/entrypoint/extract/create-enrich-draft-components-input.ts similarity index 85% rename from packages/riviere-cli/src/features/extract/entrypoint/extract/create-enrich-draft-components-input.ts rename to apps/cli/src/features/extract/entrypoint/extract/create-enrich-draft-components-input.ts index 6d03ed7f6..5d2245c82 100644 --- a/packages/riviere-cli/src/features/extract/entrypoint/extract/create-enrich-draft-components-input.ts +++ b/apps/cli/src/features/extract/entrypoint/extract/create-enrich-draft-components-input.ts @@ -1,4 +1,4 @@ -import type { EnrichDraftComponentsInput } from '../../commands/enrich-draft-components-input' +import type { EnrichDraftComponentsInput } from '@living-architecture/riviere-extract-ts-use-cases/features/extract/commands/enrich-draft-components-input' interface EnrichDraftComponentsFactoryInput { allowIncomplete?: boolean diff --git a/packages/riviere-cli/src/features/extract/entrypoint/extract/create-extract-draft-components-input.ts b/apps/cli/src/features/extract/entrypoint/extract/create-extract-draft-components-input.ts similarity index 89% rename from packages/riviere-cli/src/features/extract/entrypoint/extract/create-extract-draft-components-input.ts rename to apps/cli/src/features/extract/entrypoint/extract/create-extract-draft-components-input.ts index 55fbb06eb..6a24d227f 100644 --- a/packages/riviere-cli/src/features/extract/entrypoint/extract/create-extract-draft-components-input.ts +++ b/apps/cli/src/features/extract/entrypoint/extract/create-extract-draft-components-input.ts @@ -1,4 +1,4 @@ -import type { ExtractDraftComponentsInput } from '../../commands/extract-draft-components-input' +import type { ExtractDraftComponentsInput } from '@living-architecture/riviere-extract-ts-use-cases/features/extract/commands/extract-draft-components-input' interface ExtractDraftComponentsFactoryInput { allowIncomplete?: boolean diff --git a/apps/cli/src/features/extract/entrypoint/extract/entrypoint.ts b/apps/cli/src/features/extract/entrypoint/extract/entrypoint.ts new file mode 100644 index 000000000..b5647400e --- /dev/null +++ b/apps/cli/src/features/extract/entrypoint/extract/entrypoint.ts @@ -0,0 +1,95 @@ +import { Command } from 'commander' +import { CliErrorCode, ExitCode } from '../../../../infra/cli/presentation/error-codes' +import { exitWithCliError } from '../../../../infra/cli/presentation/exit-with-cli-error' +import { validateFlagCombinations } from './extract-validator' +import type { EnrichDraftComponents } from '@living-architecture/riviere-extract-ts-use-cases/features/extract/commands/enrich-draft-components' +import type { ExtractDraftComponents } from '@living-architecture/riviere-extract-ts-use-cases/features/extract/commands/extract-draft-components' +import { createExtractDraftComponentsInput } from './create-extract-draft-components-input' +import { createEnrichDraftComponentsInput } from './create-enrich-draft-components-input' +import { dataAccessCliErrorCode, presentExtractionResult } from './present-extraction-result' + +/** @riviere-role cli-entrypoint */ +export function createExtractCommand( + extractDraftComponents: Pick, + enrichDraftComponents: Pick, +): Command { + return new Command('extract') + .description('Extract architectural components from source code') + .requiredOption('--config ', 'Path to extraction config file') + .option('--dry-run', 'Show component counts per domain without full output') + .option('-o, --output ', 'Write output to file instead of stdout') + .option('--components-only', 'Output only component identity (no metadata enrichment)') + .option('--enrich ', 'Read draft components from file and enrich with extraction rules') + .option('--allow-incomplete', 'Output components even when some extraction fields fail') + .option('--pr', 'Extract from files changed in current branch vs base branch') + .option('--base ', 'Override base branch for --pr (default: auto-detect)') + .option('--files ', 'Extract from specific files') + .option('--format ', 'Output format: json (default) or markdown') + .option('--stats', 'Show extraction statistics on stderr') + .option('--no-ts-config', 'Skip tsconfig.json auto-discovery (disables full type resolution)') + .action( + (options: { + allowIncomplete?: boolean + base?: string + componentsOnly?: boolean + config: string + dryRun?: boolean + enrich?: string + files?: string[] + format?: string + output?: string + pr?: boolean + stats?: boolean + tsConfig?: boolean + }) => { + validateFlagCombinations(options) + + const result = + options.enrich === undefined + ? extractDraftComponents.execute(createExtractDraftComponentsInput(options)) + : enrichDraftComponents.execute( + createEnrichDraftComponentsInput(options, options.enrich), + ) + + if (result.kind === 'fieldFailure') { + exitWithCliError( + CliErrorCode.ValidationError, + `Extraction failed for fields: ${result.failedFields.join(', ')}`, + ExitCode.ExtractionFailure, + [], + ) + } + + if (result.kind === 'configFailure') { + exitWithCliError( + result.code === 'CONFIG_NOT_FOUND' + ? CliErrorCode.ConfigNotFound + : CliErrorCode.ValidationError, + result.message, + ExitCode.ConfigValidation, + [], + ) + } + + if (result.kind === 'connectionDetectionFailure') { + exitWithCliError( + CliErrorCode.ConnectionDetectionFailure, + result.message, + ExitCode.ExtractionFailure, + ['Use --allow-incomplete to emit uncertain links instead of failing'], + ) + } + + if (result.kind === 'dataAccessFailure') { + exitWithCliError( + dataAccessCliErrorCode(result.code), + result.message, + ExitCode.RuntimeError, + [], + ) + } + + presentExtractionResult(result, options) + }, + ) +} diff --git a/packages/riviere-cli/src/features/extract/entrypoint/extract/extract-output-formatter.spec.ts b/apps/cli/src/features/extract/entrypoint/extract/extract-output-formatter.spec.ts similarity index 96% rename from packages/riviere-cli/src/features/extract/entrypoint/extract/extract-output-formatter.spec.ts rename to apps/cli/src/features/extract/entrypoint/extract/extract-output-formatter.spec.ts index 30162c9a3..00fce991b 100644 --- a/packages/riviere-cli/src/features/extract/entrypoint/extract/extract-output-formatter.spec.ts +++ b/apps/cli/src/features/extract/entrypoint/extract/extract-output-formatter.spec.ts @@ -1,6 +1,4 @@ -import { - describe, expect, it -} from 'vitest' +import { describe, expect, it } from 'vitest' import { formatDryRunOutput } from './extract-output-formatter' describe('formatDryRunOutput', () => { diff --git a/packages/riviere-cli/src/features/extract/entrypoint/extract/extract-output-formatter.ts b/apps/cli/src/features/extract/entrypoint/extract/extract-output-formatter.ts similarity index 76% rename from packages/riviere-cli/src/features/extract/entrypoint/extract/extract-output-formatter.ts rename to apps/cli/src/features/extract/entrypoint/extract/extract-output-formatter.ts index cef6d9fdd..c2e5987c6 100644 --- a/packages/riviere-cli/src/features/extract/entrypoint/extract/extract-output-formatter.ts +++ b/apps/cli/src/features/extract/entrypoint/extract/extract-output-formatter.ts @@ -1,11 +1,16 @@ -import { type DraftComponent } from '@living-architecture/riviere-extract-ts' +import type { ExtractDraftComponentsResult } from '@living-architecture/riviere-extract-ts-use-cases/features/extract/commands/extract-draft-components-result' + +type DraftComponent = Extract< + ExtractDraftComponentsResult, + { kind: 'draftOnly' } +>['components'][number] function compareByCodePoint(a: string, b: string): number { return a.localeCompare(b) } /** @riviere-role cli-output-formatter */ -export function formatDryRunOutput(components: DraftComponent[]): string[] { +export function formatDryRunOutput(components: readonly DraftComponent[]): string[] { const countsByDomain = new Map>() for (const component of components) { diff --git a/packages/riviere-cli/src/features/extract/entrypoint/extract/extract-validator.ts b/apps/cli/src/features/extract/entrypoint/extract/extract-validator.ts similarity index 94% rename from packages/riviere-cli/src/features/extract/entrypoint/extract/extract-validator.ts rename to apps/cli/src/features/extract/entrypoint/extract/extract-validator.ts index 50cc74884..ad9300ddc 100644 --- a/packages/riviere-cli/src/features/extract/entrypoint/extract/extract-validator.ts +++ b/apps/cli/src/features/extract/entrypoint/extract/extract-validator.ts @@ -1,7 +1,4 @@ -import { - CliErrorCode, - ConfigValidationError, -} from '../../../../platform/infra/cli/presentation/error-codes' +import { CliErrorCode, ConfigValidationError } from '../../../../infra/cli/presentation/error-codes' interface ExtractOptions { allowIncomplete?: boolean diff --git a/packages/riviere-cli/src/features/extract/entrypoint/extract/extract.connections.spec.ts b/apps/cli/src/features/extract/entrypoint/extract/extract.connections.spec.ts similarity index 92% rename from packages/riviere-cli/src/features/extract/entrypoint/extract/extract.connections.spec.ts rename to apps/cli/src/features/extract/entrypoint/extract/extract.connections.spec.ts index 0cec3566f..7f1783257 100644 --- a/packages/riviere-cli/src/features/extract/entrypoint/extract/extract.connections.spec.ts +++ b/apps/cli/src/features/extract/entrypoint/extract/extract.connections.spec.ts @@ -1,19 +1,17 @@ -import { - describe, it, expect, vi, afterEach -} from 'vitest' -import type { TestContext } from '../../../../platform/__fixtures__/command-test-fixtures' +import { describe, it, expect, vi, afterEach } from 'vitest' +import type { TestContext } from '../../../../__fixtures__/command-test-fixtures' import { createTestContext, setupCommandTest, assertDefined, -} from '../../../../platform/__fixtures__/command-test-fixtures' +} from '../../../../__fixtures__/command-test-fixtures' import { createProgram } from '../../../../shell/cli' import { parseFullExtractionOutput, createValidExtractFixture, } from '../../__fixtures__/extraction-test-fixtures' -vi.mock('../../../../platform/infra/external-clients/git/git-repository-info', () => ({ +vi.mock('../../../../infra/external-clients/git/git-repository-info', () => ({ getRepositoryInfo: vi.fn(() => ({ name: 'test/repo', owner: 'test', @@ -127,9 +125,7 @@ describe('riviere extract — connection detection', () => { setupCommandTest(ctx) it('outputs empty links when zero components extracted', async () => { - const { - writeFile, mkdir - } = await import('node:fs/promises') + const { writeFile, mkdir } = await import('node:fs/promises') const { join } = await import('node:path') const srcDir = join(ctx.testDir, 'src') await mkdir(srcDir, { recursive: true }) diff --git a/packages/riviere-cli/src/features/extract/entrypoint/extract/extract.dry-run.spec.ts b/apps/cli/src/features/extract/entrypoint/extract/extract.dry-run.spec.ts similarity index 93% rename from packages/riviere-cli/src/features/extract/entrypoint/extract/extract.dry-run.spec.ts rename to apps/cli/src/features/extract/entrypoint/extract/extract.dry-run.spec.ts index 0ed1492f2..f9c1abe07 100644 --- a/packages/riviere-cli/src/features/extract/entrypoint/extract/extract.dry-run.spec.ts +++ b/apps/cli/src/features/extract/entrypoint/extract/extract.dry-run.spec.ts @@ -1,20 +1,16 @@ -import { - writeFile, mkdir -} from 'node:fs/promises' +import { writeFile, mkdir } from 'node:fs/promises' import { join } from 'node:path' -import { - describe, it, expect -} from 'vitest' +import { describe, it, expect } from 'vitest' import { createProgram } from '../../../../shell/cli' -import type { TestContext } from '../../../../platform/__fixtures__/command-test-fixtures' +import type { TestContext } from '../../../../__fixtures__/command-test-fixtures' import { createTestContext, setupCommandTest, assertDefined, -} from '../../../../platform/__fixtures__/command-test-fixtures' +} from '../../../../__fixtures__/command-test-fixtures' import { parseFullExtractionOutput } from '../../__fixtures__/extraction-test-fixtures' -vi.mock('../../../../platform/infra/external-clients/git/git-repository-info', () => ({ +vi.mock('../../../../infra/external-clients/git/git-repository-info', () => ({ getRepositoryInfo: vi.fn(() => ({ name: 'test/repo', owner: 'test', @@ -116,6 +112,8 @@ modules: where: hasJSDoc: tag: api + extract: + apiType: { literal: REST } useCase: find: classes where: @@ -194,6 +192,8 @@ modules: where: hasJSDoc: tag: api + extract: + apiType: { literal: REST } useCase: { notUsed: true } domainOp: { notUsed: true } event: { notUsed: true } @@ -253,6 +253,8 @@ modules: where: hasJSDoc: tag: api + extract: + apiType: { literal: REST } useCase: find: classes where: @@ -321,6 +323,8 @@ modules: where: hasJSDoc: tag: api + extract: + apiType: { literal: REST } useCase: find: classes where: diff --git a/packages/riviere-cli/src/features/extract/entrypoint/extract/extract.enrichment.spec.ts b/apps/cli/src/features/extract/entrypoint/extract/extract.enrichment.spec.ts similarity index 90% rename from packages/riviere-cli/src/features/extract/entrypoint/extract/extract.enrichment.spec.ts rename to apps/cli/src/features/extract/entrypoint/extract/extract.enrichment.spec.ts index 1d2235308..6c4796154 100644 --- a/packages/riviere-cli/src/features/extract/entrypoint/extract/extract.enrichment.spec.ts +++ b/apps/cli/src/features/extract/entrypoint/extract/extract.enrichment.spec.ts @@ -1,24 +1,19 @@ -import { - writeFile, mkdir -} from 'node:fs/promises' +import { writeFile, mkdir } from 'node:fs/promises' import { join } from 'node:path' -import { - describe, it, expect -} from 'vitest' -import type { DraftComponent } from '@living-architecture/riviere-extract-ts' -import type { TestContext } from '../../../../platform/__fixtures__/command-test-fixtures' +import { describe, it, expect } from 'vitest' +import type { TestContext } from '../../../../__fixtures__/command-test-fixtures' import { createTestContext, setupCommandTest, parseErrorOutput, parseCommandWithErrorHandling, -} from '../../../../platform/__fixtures__/command-test-fixtures' +} from '../../../../__fixtures__/command-test-fixtures' import { parseFullExtractionOutput, validSourceCode, } from '../../__fixtures__/extraction-test-fixtures' -vi.mock('../../../../platform/infra/external-clients/git/git-repository-info', () => ({ +vi.mock('../../../../infra/external-clients/git/git-repository-info', () => ({ getRepositoryInfo: vi.fn(() => ({ name: 'test/repo', owner: 'test', @@ -131,11 +126,12 @@ describe('riviere extract enrichment', () => { const configPath = join(ctx.testDir, 'extract.yaml') await writeFile(configPath, configWithLiteralExtract) - const draftComponents: DraftComponent[] = [ + const draftComponents = [ { type: 'useCase', name: 'PlaceOrder', domain: 'orders', + module: 'orders', location: { file: join(srcDir, 'order-service.ts'), line: 2, diff --git a/packages/riviere-cli/src/features/extract/entrypoint/extract/extract.pr-extraction.spec.ts b/apps/cli/src/features/extract/entrypoint/extract/extract.pr-extraction.spec.ts similarity index 83% rename from packages/riviere-cli/src/features/extract/entrypoint/extract/extract.pr-extraction.spec.ts rename to apps/cli/src/features/extract/entrypoint/extract/extract.pr-extraction.spec.ts index 200108100..1511c20b2 100644 --- a/packages/riviere-cli/src/features/extract/entrypoint/extract/extract.pr-extraction.spec.ts +++ b/apps/cli/src/features/extract/entrypoint/extract/extract.pr-extraction.spec.ts @@ -1,50 +1,29 @@ -import { writeFile } from 'node:fs/promises' +import { appendFile, rm, writeFile } from 'node:fs/promises' import { join } from 'node:path' -import { - describe, it, expect, vi -} from 'vitest' -import type { TestContext } from '../../../../platform/__fixtures__/command-test-fixtures' +import { describe, it, expect, vi } from 'vitest' +import type { TestContext } from '../../../../__fixtures__/command-test-fixtures' import { createTestContext, setupCommandTest, parseErrorOutput, parseCommandWithErrorHandling, -} from '../../../../platform/__fixtures__/command-test-fixtures' -import { CliErrorCode } from '../../../../platform/infra/cli/presentation/error-codes' + runIsolatedGit, +} from '../../../../__fixtures__/command-test-fixtures' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' import { parseExtractionOutput, parseFullExtractionOutput, createValidExtractFixture, } from '../../__fixtures__/extraction-test-fixtures' -vi.mock('../../../../platform/infra/external-clients/git/git-repository-info', () => ({ - getRepositoryInfo: vi.fn(() => ({ - name: 'test/repo', - owner: 'test', - url: 'https://github.com/test/repo.git', - })), -})) - -vi.mock( - '../../../../platform/infra/external-clients/git/git-changed-files', - async (importOriginal) => { - const original = - await importOriginal< - typeof import('../../../../platform/infra/external-clients/git/git-changed-files') - >() - return { - ...original, - detectChangedTypeScriptFiles: vi.fn(), - } - }, -) - -import { - detectChangedTypeScriptFiles, - GitError, -} from '../../../../platform/infra/external-clients/git/git-changed-files' - -const mockDetectChanged = vi.mocked(detectChangedTypeScriptFiles) +async function createFeatureBranchChange(directory: string, sourceFile: string): Promise { + runIsolatedGit(directory, ['add', '.']) + runIsolatedGit(directory, ['commit', '-m', 'base']) + runIsolatedGit(directory, ['checkout', '-b', 'feature']) + await appendFile(sourceFile, '\n// feature branch change\n') + runIsolatedGit(directory, ['add', sourceFile]) + runIsolatedGit(directory, ['commit', '-m', 'change source']) +} describe('riviere extract PR extraction', () => { describe('flag mutual exclusivity', () => { @@ -67,9 +46,7 @@ describe('riviere extract PR extraction', () => { expectedA: '--files', expectedB: '--enrich', }, - ])('rejects $expectedA and $expectedB together', async ({ - flags, expectedA, expectedB - }) => { + ])('rejects $expectedA and $expectedB together', async ({ flags, expectedA, expectedB }) => { const configPath = await createValidExtractFixture(ctx.testDir) await expect( @@ -181,6 +158,7 @@ describe('riviere extract PR extraction', () => { it('extracts components from specified files only', async () => { const configPath = await createValidExtractFixture(ctx.testDir) const sourceFile = join(ctx.testDir, 'src', 'order-service.ts') + await createFeatureBranchChange(ctx.testDir, sourceFile) await parseCommandWithErrorHandling([ 'node', @@ -204,6 +182,8 @@ describe('riviere extract PR extraction', () => { it('returns empty when specified file is not in config glob', async () => { const configPath = await createValidExtractFixture(ctx.testDir) + const sourceFile = join(ctx.testDir, 'src', 'order-service.ts') + await createFeatureBranchChange(ctx.testDir, sourceFile) const outsideFile = join(ctx.testDir, 'outside.ts') await writeFile(outsideFile, 'export const x = 1') @@ -225,6 +205,7 @@ describe('riviere extract PR extraction', () => { it('outputs markdown when --format markdown used with --files', async () => { const configPath = await createValidExtractFixture(ctx.testDir) const sourceFile = join(ctx.testDir, 'src', 'order-service.ts') + await createFeatureBranchChange(ctx.testDir, sourceFile) await parseCommandWithErrorHandling([ 'node', @@ -251,9 +232,7 @@ describe('riviere extract PR extraction', () => { it('handles git error when not in a git repo', async () => { const configPath = await createValidExtractFixture(ctx.testDir) - mockDetectChanged.mockImplementation(() => { - throw new GitError('NOT_A_REPOSITORY', 'Run from within a git repository.') - }) + await rm(join(ctx.testDir, '.git'), { recursive: true }) await expect( parseCommandWithErrorHandling([ @@ -273,10 +252,7 @@ describe('riviere extract PR extraction', () => { it('extracts components from changed files on feature branch', async () => { const configPath = await createValidExtractFixture(ctx.testDir) const sourceFile = join(ctx.testDir, 'src', 'order-service.ts') - mockDetectChanged.mockReturnValue({ - files: [sourceFile], - warnings: [], - }) + await createFeatureBranchChange(ctx.testDir, sourceFile) await parseCommandWithErrorHandling([ 'node', @@ -303,10 +279,8 @@ describe('riviere extract PR extraction', () => { it('warns about untracked TypeScript files', async () => { const configPath = await createValidExtractFixture(ctx.testDir) const sourceFile = join(ctx.testDir, 'src', 'order-service.ts') - mockDetectChanged.mockReturnValue({ - files: [sourceFile], - warnings: ['1 untracked TypeScript file(s) not included: untracked.ts'], - }) + await createFeatureBranchChange(ctx.testDir, sourceFile) + await writeFile(join(ctx.testDir, 'untracked.ts'), 'export {}') const stderrOutput: string[] = [] const errorSpy = vi.spyOn(console, 'error').mockImplementation((msg: string) => { @@ -333,10 +307,7 @@ describe('riviere extract PR extraction', () => { it('outputs markdown format for --pr with --format markdown', async () => { const configPath = await createValidExtractFixture(ctx.testDir) const sourceFile = join(ctx.testDir, 'src', 'order-service.ts') - mockDetectChanged.mockReturnValue({ - files: [sourceFile], - warnings: [], - }) + await createFeatureBranchChange(ctx.testDir, sourceFile) await parseCommandWithErrorHandling([ 'node', diff --git a/packages/riviere-cli/src/features/extract/entrypoint/extract/extract.spec.ts b/apps/cli/src/features/extract/entrypoint/extract/extract.spec.ts similarity index 84% rename from packages/riviere-cli/src/features/extract/entrypoint/extract/extract.spec.ts rename to apps/cli/src/features/extract/entrypoint/extract/extract.spec.ts index d3d33aeda..6b00fc375 100644 --- a/packages/riviere-cli/src/features/extract/entrypoint/extract/extract.spec.ts +++ b/apps/cli/src/features/extract/entrypoint/extract/extract.spec.ts @@ -1,26 +1,25 @@ -import { - readFile, writeFile, mkdir -} from 'node:fs/promises' +import { readFile, writeFile, mkdir } from 'node:fs/promises' import { join } from 'node:path' -import { - describe, it, expect -} from 'vitest' +import { describe, it, expect } from 'vitest' import { createProgram } from '../../../../shell/cli' -import type { TestContext } from '../../../../platform/__fixtures__/command-test-fixtures' +import type { TestContext } from '../../../../__fixtures__/command-test-fixtures' import { createTestContext, setupCommandTest, parseErrorOutput, parseCommandWithErrorHandling, -} from '../../../../platform/__fixtures__/command-test-fixtures' -import { CliErrorCode } from '../../../../platform/infra/cli/presentation/error-codes' +} from '../../../../__fixtures__/command-test-fixtures' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' import { parseExtractionOutput, parseFullExtractionOutput, createValidExtractFixture, } from '../../__fixtures__/extraction-test-fixtures' +import type { ExtractDraftComponents } from '@living-architecture/riviere-extract-ts-use-cases/features/extract/commands/extract-draft-components' +import type { EnrichDraftComponents } from '@living-architecture/riviere-extract-ts-use-cases/features/extract/commands/enrich-draft-components' +import { createExtractCommand } from './entrypoint' -vi.mock('../../../../platform/infra/external-clients/git/git-repository-info', () => ({ +vi.mock('../../../../infra/external-clients/git/git-repository-info', () => ({ getRepositoryInfo: vi.fn(() => ({ name: 'test/repo', owner: 'test', @@ -41,6 +40,40 @@ describe('riviere extract', () => { }) }) + describe('connection detection errors', () => { + const ctx: TestContext = createTestContext() + setupCommandTest(ctx) + + it('returns the connection failure with the incomplete-link suggestion', async () => { + const extractDraftComponents: Pick = { + execute: () => ({ + kind: 'connectionDetectionFailure', + message: 'Could not resolve OrderId', + }), + } + const enrichDraftComponents: Pick = { + execute: () => ({ + kind: 'connectionDetectionFailure', + message: 'Could not resolve OrderId', + }), + } + + await expect( + createExtractCommand(extractDraftComponents, enrichDraftComponents).parseAsync( + ['--config', 'extract.yaml'], + { from: 'user' }, + ), + ).rejects.toMatchObject({ exitCode: 1 }) + + const output = parseErrorOutput(ctx.consoleOutput) + expect(output.error.code).toBe(CliErrorCode.ConnectionDetectionFailure) + expect(output.error.message).toBe('Could not resolve OrderId') + expect(output.error.suggestions).toStrictEqual([ + 'Use --allow-incomplete to emit uncertain links instead of failing', + ]) + }) + }) + describe('config file errors', () => { const ctx: TestContext = createTestContext() setupCommandTest(ctx) diff --git a/packages/riviere-cli/src/features/extract/entrypoint/extract/format-extraction-stats.spec.ts b/apps/cli/src/features/extract/entrypoint/extract/format-extraction-stats.spec.ts similarity index 98% rename from packages/riviere-cli/src/features/extract/entrypoint/extract/format-extraction-stats.spec.ts rename to apps/cli/src/features/extract/entrypoint/extract/format-extraction-stats.spec.ts index 97c9d97e7..655c8a01f 100644 --- a/packages/riviere-cli/src/features/extract/entrypoint/extract/format-extraction-stats.spec.ts +++ b/apps/cli/src/features/extract/entrypoint/extract/format-extraction-stats.spec.ts @@ -1,6 +1,4 @@ -import { - describe, it, expect -} from 'vitest' +import { describe, it, expect } from 'vitest' import { countLinksByType, formatExtractionStats, diff --git a/packages/riviere-cli/src/features/extract/entrypoint/extract/format-extraction-stats.ts b/apps/cli/src/features/extract/entrypoint/extract/format-extraction-stats.ts similarity index 80% rename from packages/riviere-cli/src/features/extract/entrypoint/extract/format-extraction-stats.ts rename to apps/cli/src/features/extract/entrypoint/extract/format-extraction-stats.ts index 44e763ca8..7faca1177 100644 --- a/packages/riviere-cli/src/features/extract/entrypoint/extract/format-extraction-stats.ts +++ b/apps/cli/src/features/extract/entrypoint/extract/format-extraction-stats.ts @@ -1,6 +1,8 @@ -import type { - ConnectionTimings, ExtractedLink -} from '@living-architecture/riviere-extract-ts' +import type { ExtractDraftComponentsResult } from '@living-architecture/riviere-extract-ts-use-cases/features/extract/commands/extract-draft-components-result' + +type FullExtractionResult = Extract +type ConnectionTimings = FullExtractionResult['timings'][number] +type ExtractedLink = FullExtractionResult['links'][number] interface ExtractionStatsInput { componentCount: number diff --git a/apps/cli/src/features/extract/entrypoint/extract/present-extraction-result.spec.ts b/apps/cli/src/features/extract/entrypoint/extract/present-extraction-result.spec.ts new file mode 100644 index 000000000..292aa0949 --- /dev/null +++ b/apps/cli/src/features/extract/entrypoint/extract/present-extraction-result.spec.ts @@ -0,0 +1,73 @@ +import { describe, expect, it, vi } from 'vitest' +import { dataAccessCliErrorCode, presentExtractionResult } from './present-extraction-result' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' + +describe('dataAccessCliErrorCode', () => { + it('maps GIT_NOT_FOUND to the CLI git-not-found code', () => { + expect(dataAccessCliErrorCode('GIT_NOT_FOUND')).toBe(CliErrorCode.GitNotFound) + }) + + it('maps other data-access failures to validation errors', () => { + expect(dataAccessCliErrorCode('FILE_READ_ERROR')).toBe(CliErrorCode.ValidationError) + }) +}) + +describe('presentExtractionResult', () => { + it('returns early for fieldFailure results', () => { + const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined) + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined) + + presentExtractionResult( + { + failedFields: ['name'], + kind: 'fieldFailure', + }, + {}, + ) + + expect(logSpy).not.toHaveBeenCalled() + expect(errorSpy).not.toHaveBeenCalled() + + logSpy.mockRestore() + errorSpy.mockRestore() + }) + + it('returns early for configFailure results', () => { + const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined) + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined) + + presentExtractionResult( + { + code: 'VALIDATION_ERROR', + kind: 'configFailure', + message: 'Invalid config', + }, + {}, + ) + + expect(logSpy).not.toHaveBeenCalled() + expect(errorSpy).not.toHaveBeenCalled() + + logSpy.mockRestore() + errorSpy.mockRestore() + }) + + it('returns early for connectionDetectionFailure results', () => { + const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined) + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined) + + presentExtractionResult( + { + kind: 'connectionDetectionFailure', + message: 'Could not resolve type', + }, + {}, + ) + + expect(logSpy).not.toHaveBeenCalled() + expect(errorSpy).not.toHaveBeenCalled() + + logSpy.mockRestore() + errorSpy.mockRestore() + }) +}) diff --git a/apps/cli/src/features/extract/entrypoint/extract/present-extraction-result.ts b/apps/cli/src/features/extract/entrypoint/extract/present-extraction-result.ts new file mode 100644 index 000000000..f73459fef --- /dev/null +++ b/apps/cli/src/features/extract/entrypoint/extract/present-extraction-result.ts @@ -0,0 +1,113 @@ +import { categorizeComponents } from './categorize-components' +import { + countLinksByType, + formatExtractionStats, + formatTimingLine, +} from './format-extraction-stats' +import { formatDryRunOutput } from './extract-output-formatter' +import { formatPrMarkdown } from '../../../../infra/cli/presentation/format-pr-markdown' +import { formatSuccess } from '../../../../infra/cli/presentation/output' +import { outputResult } from '../../../../infra/cli/presentation/output-writer' +import type { EnrichDraftComponentsResult } from '@living-architecture/riviere-extract-ts-use-cases/features/extract/commands/enrich-draft-components-result' +import type { ExtractDraftComponentsResult } from '@living-architecture/riviere-extract-ts-use-cases/features/extract/commands/extract-draft-components-result' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' + +type ExtractionResult = ExtractDraftComponentsResult | EnrichDraftComponentsResult +type ExtractionPresentationOptions = { + dryRun?: boolean + format?: string + output?: string + stats?: boolean +} + +/** @riviere-role cli-output-formatter */ +export function dataAccessCliErrorCode( + code: Extract['code'], +): CliErrorCode { + switch (code) { + case 'GIT_NOT_FOUND': + return CliErrorCode.GitNotFound + case 'NOT_A_REPOSITORY': + return CliErrorCode.GitNotARepository + default: + return CliErrorCode.ValidationError + } +} + +/** @riviere-role cli-output-formatter */ +export function presentExtractionResult( + result: ExtractionResult, + options: ExtractionPresentationOptions, +): void { + if (result.kind === 'draftOnly') { + presentDraftResult(result.components, options) + return + } + + if ( + result.kind === 'fieldFailure' || + result.kind === 'configFailure' || + result.kind === 'dataAccessFailure' || + result.kind === 'connectionDetectionFailure' + ) { + return + } + + presentFullResult(result, options) +} + +function presentDraftResult( + components: Extract['components'], + options: ExtractionPresentationOptions, +): void { + /* v8 ignore start -- @preserve: dry-run tested via CLI integration */ + if (options.dryRun) { + for (const line of formatDryRunOutput(components)) { + console.log(line) + } + return + } + /* v8 ignore stop */ + + if (options.format === 'markdown') { + const markdown = formatPrMarkdown(categorizeComponents(components, undefined)) + console.log(markdown) + return + } + + outputResult(formatSuccess(components), createOutputOptions(options.output)) +} + +function presentFullResult( + result: Extract, + options: ExtractionPresentationOptions, +): void { + if (result.failedFields.length > 0) { + console.error( + `Warning: Enrichment failed for ${result.failedFields.length} field(s): ${result.failedFields.join(', ')}`, + ) + } + + if (options.stats === true) { + for (const timing of result.timings) { + console.error(formatTimingLine(timing)) + } + const stats = countLinksByType(result.components.length, result.links) + for (const line of formatExtractionStats(stats)) { + console.error(line) + } + } + + outputResult( + formatSuccess({ + components: result.components, + links: result.links, + externalLinks: result.externalLinks, + }), + createOutputOptions(options.output), + ) +} + +function createOutputOptions(outputPath: string | undefined): { output?: string } { + return outputPath === undefined ? {} : { output: outputPath } +} diff --git a/apps/cli/src/features/query/entrypoint/_platform/cli/component-output.ts b/apps/cli/src/features/query/entrypoint/_platform/cli/component-output.ts new file mode 100644 index 000000000..0da749334 --- /dev/null +++ b/apps/cli/src/features/query/entrypoint/_platform/cli/component-output.ts @@ -0,0 +1,16 @@ +interface ComponentOutput { + id: string + type: string + name: string + domain: string +} + +/** @riviere-role cli-output-formatter */ +export function toComponentOutput(component: ComponentOutput): ComponentOutput { + return { + id: component.id, + type: component.type, + name: component.name, + domain: component.domain, + } +} diff --git a/packages/riviere-cli/src/features/query/entrypoint/components/components.errors.spec.ts b/apps/cli/src/features/query/entrypoint/components/components.errors.spec.ts similarity index 87% rename from packages/riviere-cli/src/features/query/entrypoint/components/components.errors.spec.ts rename to apps/cli/src/features/query/entrypoint/components/components.errors.spec.ts index 56c0f047f..b891d632c 100644 --- a/packages/riviere-cli/src/features/query/entrypoint/components/components.errors.spec.ts +++ b/apps/cli/src/features/query/entrypoint/components/components.errors.spec.ts @@ -1,14 +1,12 @@ -import { - describe, it, expect, vi -} from 'vitest' +import { describe, it, expect, vi } from 'vitest' import { createProgram } from '../../../../shell/cli' -import { CliErrorCode } from '../../../../platform/infra/cli/presentation/error-codes' -import type { TestContext } from '../../../../platform/__fixtures__/command-test-fixtures' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' +import type { TestContext } from '../../../../__fixtures__/command-test-fixtures' import { createTestContext, setupCommandTest, createGraph, -} from '../../../../platform/__fixtures__/command-test-fixtures' +} from '../../../../__fixtures__/command-test-fixtures' describe('riviere query components - error handling', () => { const ctx: TestContext = createTestContext() diff --git a/packages/riviere-cli/src/features/query/entrypoint/components/components.spec.ts b/apps/cli/src/features/query/entrypoint/components/components.spec.ts similarity index 97% rename from packages/riviere-cli/src/features/query/entrypoint/components/components.spec.ts rename to apps/cli/src/features/query/entrypoint/components/components.spec.ts index 68fe83064..fc0750ecd 100644 --- a/packages/riviere-cli/src/features/query/entrypoint/components/components.spec.ts +++ b/apps/cli/src/features/query/entrypoint/components/components.spec.ts @@ -1,15 +1,13 @@ -import { - describe, it, expect -} from 'vitest' +import { describe, it, expect } from 'vitest' import { createProgram } from '../../../../shell/cli' -import type { TestContext } from '../../../../platform/__fixtures__/command-test-fixtures' +import type { TestContext } from '../../../../__fixtures__/command-test-fixtures' import { createTestContext, setupCommandTest, createGraph, sourceLocation, TestAssertionError, -} from '../../../../platform/__fixtures__/command-test-fixtures' +} from '../../../../__fixtures__/command-test-fixtures' interface ComponentInfo { id: string diff --git a/apps/cli/src/features/query/entrypoint/components/entrypoint.ts b/apps/cli/src/features/query/entrypoint/components/entrypoint.ts new file mode 100644 index 000000000..fa832058c --- /dev/null +++ b/apps/cli/src/features/query/entrypoint/components/entrypoint.ts @@ -0,0 +1,62 @@ +import { Command } from 'commander' +import { formatSuccess, formatError } from '../../../../infra/cli/presentation/output' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' +import { formatQueryGraphLoadFailure } from '../../../../infra/cli/presentation/query-graph-load-failure-output' +import { getDefaultGraphPathDescription } from '../../../../infra/cli/presentation/graph-path-option' +import { toComponentOutput } from '../_platform/cli/component-output' +import type { ListComponents } from '@living-architecture/riviere-builder-use-cases/features/query/queries/list-components' + +interface ComponentsOptions { + graph?: string + json?: boolean + domain?: string + type?: string +} + +/** @riviere-role cli-entrypoint */ +export function createComponentsCommand(listComponents: ListComponents): Command { + return new Command('components') + .description('List components with optional filtering') + .addHelpText( + 'after', + ` +Examples: + $ riviere query components + $ riviere query components --domain orders + $ riviere query components --type API --json + $ riviere query components --domain orders --type UseCase +`, + ) + .option('--graph ', getDefaultGraphPathDescription()) + .option('--json', 'Output result as JSON') + .option('--domain ', 'Filter by domain name') + .option('--type ', 'Filter by component type') + .action(async (options: ComponentsOptions) => { + const result = listComponents.execute({ + domain: options.domain, + graphPathOption: options.graph, + type: options.type, + }) + + if ('kind' in result) { + if (result.kind === 'invalidComponentType' && options.json !== true) { + console.error(`Error: ${result.message}`) + return + } + console.log( + JSON.stringify( + result.kind === 'invalidComponentType' + ? formatError(CliErrorCode.ValidationError, result.message) + : formatQueryGraphLoadFailure(result), + ), + ) + return + } + + const components = result.components.map(toComponentOutput) + + if (options.json) { + console.log(JSON.stringify(formatSuccess({ components }))) + } + }) +} diff --git a/apps/cli/src/features/query/entrypoint/components/query-entrypoint-error-rethrow.spec.ts b/apps/cli/src/features/query/entrypoint/components/query-entrypoint-error-rethrow.spec.ts new file mode 100644 index 000000000..a2b9c68da --- /dev/null +++ b/apps/cli/src/features/query/entrypoint/components/query-entrypoint-error-rethrow.spec.ts @@ -0,0 +1,142 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' + +type Loader = () => Promise + +class UnexpectedQueryEntrypointError extends Error { + constructor(message: string) { + super(message) + this.name = 'UnexpectedQueryEntrypointError' + } +} + +async function expectRethrow< + T extends { createProgram: () => { parseAsync: (argv: string[]) => Promise } }, +>(loadModule: Loader, argv: string[]): Promise { + const module = await loadModule() + await expect(module.createProgram().parseAsync(argv)).rejects.toThrow('unexpected failure') +} + +describe('query entrypoints rethrow unknown errors', () => { + afterEach(() => { + vi.resetModules() + vi.restoreAllMocks() + vi.doUnmock( + '@living-architecture/riviere-builder-use-cases/features/query/queries/list-components', + ) + vi.doUnmock( + '@living-architecture/riviere-builder-use-cases/features/query/queries/list-domains', + ) + vi.doUnmock( + '@living-architecture/riviere-builder-use-cases/features/query/queries/list-entry-points', + ) + vi.doUnmock( + '@living-architecture/riviere-builder-use-cases/features/query/queries/detect-orphans', + ) + vi.doUnmock( + '@living-architecture/riviere-builder-use-cases/features/query/queries/search-components', + ) + vi.doUnmock('@living-architecture/riviere-builder-use-cases/features/query/queries/trace-flow') + }) + + it('rethrows unknown list-components errors', async () => { + vi.doMock( + '@living-architecture/riviere-builder-use-cases/features/query/queries/list-components', + () => ({ + ListComponents: class { + execute() { + throw new UnexpectedQueryEntrypointError('unexpected failure') + } + }, + }), + ) + await expectRethrow( + () => import('../../../../shell/cli'), + ['node', 'riviere', 'query', 'components', '--json'], + ) + }) + + it('rethrows unknown list-domains errors', async () => { + vi.doMock( + '@living-architecture/riviere-builder-use-cases/features/query/queries/list-domains', + () => ({ + ListDomains: class { + execute() { + throw new UnexpectedQueryEntrypointError('unexpected failure') + } + }, + }), + ) + await expectRethrow( + () => import('../../../../shell/cli'), + ['node', 'riviere', 'query', 'domains', '--json'], + ) + }) + + it('rethrows unknown list-entry-points errors', async () => { + vi.doMock( + '@living-architecture/riviere-builder-use-cases/features/query/queries/list-entry-points', + () => ({ + ListEntryPoints: class { + execute() { + throw new UnexpectedQueryEntrypointError('unexpected failure') + } + }, + }), + ) + await expectRethrow( + () => import('../../../../shell/cli'), + ['node', 'riviere', 'query', 'entry-points', '--json'], + ) + }) + + it('rethrows unknown orphan errors', async () => { + vi.doMock( + '@living-architecture/riviere-builder-use-cases/features/query/queries/detect-orphans', + () => ({ + DetectOrphans: class { + execute() { + throw new UnexpectedQueryEntrypointError('unexpected failure') + } + }, + }), + ) + await expectRethrow( + () => import('../../../../shell/cli'), + ['node', 'riviere', 'query', 'orphans', '--json'], + ) + }) + + it('rethrows unknown search errors', async () => { + vi.doMock( + '@living-architecture/riviere-builder-use-cases/features/query/queries/search-components', + () => ({ + SearchComponents: class { + execute() { + throw new UnexpectedQueryEntrypointError('unexpected failure') + } + }, + }), + ) + await expectRethrow( + () => import('../../../../shell/cli'), + ['node', 'riviere', 'query', 'search', 'term', '--json'], + ) + }) + + it('rethrows unknown trace errors', async () => { + vi.doMock( + '@living-architecture/riviere-builder-use-cases/features/query/queries/trace-flow', + () => ({ + TraceFlow: class { + execute() { + throw new UnexpectedQueryEntrypointError('unexpected failure') + } + }, + }), + ) + await expectRethrow( + () => import('../../../../shell/cli'), + ['node', 'riviere', 'query', 'trace', 'orders:mod:api:test', '--json'], + ) + }) +}) diff --git a/apps/cli/src/features/query/entrypoint/domains/domains.spec.ts b/apps/cli/src/features/query/entrypoint/domains/domains.spec.ts new file mode 100644 index 000000000..5aa8f03a4 --- /dev/null +++ b/apps/cli/src/features/query/entrypoint/domains/domains.spec.ts @@ -0,0 +1,208 @@ +import { describe, it, expect } from 'vitest' +import { createProgram } from '../../../../shell/cli' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' +import type { TestContext } from '../../../../__fixtures__/command-test-fixtures' +import { + createTestContext, + setupCommandTest, + createGraph, + sourceLocation, + TestAssertionError, +} from '../../../../__fixtures__/command-test-fixtures' + +interface ComponentCounts { + UI: number + API: number + UseCase: number + DomainOp: number + Event: number + EventHandler: number + Custom: number + total: number +} + +interface DomainInfo { + name: string + description: string + systemType: string + componentCounts: ComponentCounts +} + +interface DomainsOutput { + success: true + data: { domains: DomainInfo[] } + warnings: string[] +} + +function isDomainsOutput(value: unknown): value is DomainsOutput { + if (typeof value !== 'object' || value === null) return false + if (!('success' in value) || value.success !== true) return false + if (!('data' in value) || typeof value.data !== 'object' || value.data === null) return false + if (!('domains' in value.data) || !Array.isArray(value.data.domains)) return false + return true +} + +function parseOutput(consoleOutput: string[]): DomainsOutput { + const parsed: unknown = JSON.parse(consoleOutput[0] ?? '{}') + if (!isDomainsOutput(parsed)) { + throw new TestAssertionError(`Invalid domains output: ${consoleOutput[0]}`) + } + return parsed +} + +describe('riviere query domains', () => { + describe('command registration', () => { + it('registers domains command under query', () => { + const program = createProgram() + const queryCmd = program.commands.find((cmd) => cmd.name() === 'query') + const domainsCmd = queryCmd?.commands.find((cmd) => cmd.name() === 'domains') + expect(domainsCmd?.name()).toBe('domains') + }) + }) + + describe('querying domains', () => { + const ctx: TestContext = createTestContext() + setupCommandTest(ctx) + + it('returns domain names with component counts', async () => { + await createGraph(ctx.testDir, { + version: '1.0', + metadata: { + sources: [{ repository: 'https://github.com/org/repo' }], + domains: { + orders: { + description: 'Order management', + systemType: 'domain', + }, + }, + }, + components: [ + { + id: 'orders:checkout:api:place-order', + type: 'API', + name: 'place-order', + domain: 'orders', + module: 'checkout', + sourceLocation, + apiType: 'REST', + httpMethod: 'POST', + path: '/orders', + }, + { + id: 'orders:checkout:usecase:place-order', + type: 'UseCase', + name: 'place-order', + domain: 'orders', + module: 'checkout', + sourceLocation, + }, + { + id: 'orders:checkout:usecase:cancel-order', + type: 'UseCase', + name: 'cancel-order', + domain: 'orders', + module: 'checkout', + sourceLocation, + }, + ], + links: [], + }) + + await createProgram().parseAsync(['node', 'riviere', 'query', 'domains', '--json']) + const output = parseOutput(ctx.consoleOutput) + expect(output.success).toBe(true) + expect(output.data.domains).toHaveLength(1) + expect(output.data.domains[0]).toMatchObject({ + name: 'orders', + description: 'Order management', + systemType: 'domain', + componentCounts: { + API: 1, + UseCase: 2, + total: 3, + }, + }) + }) + + it('returns all domains from graph metadata', async () => { + await createGraph(ctx.testDir, { + version: '1.0', + metadata: { + sources: [{ repository: 'https://github.com/org/repo' }], + domains: { + orders: { + description: 'Order management', + systemType: 'domain', + }, + payments: { + description: 'Payment processing', + systemType: 'bff', + }, + }, + }, + components: [ + { + id: 'orders:checkout:api:place-order', + type: 'API', + name: 'place-order', + domain: 'orders', + module: 'checkout', + sourceLocation, + apiType: 'REST', + httpMethod: 'POST', + path: '/orders', + }, + { + id: 'payments:billing:api:process-payment', + type: 'API', + name: 'process-payment', + domain: 'payments', + module: 'billing', + sourceLocation, + apiType: 'REST', + httpMethod: 'POST', + path: '/payments', + }, + ], + links: [], + }) + + await createProgram().parseAsync(['node', 'riviere', 'query', 'domains', '--json']) + const output = parseOutput(ctx.consoleOutput) + expect(output.data.domains).toHaveLength(2) + expect( + output.data.domains.map((d) => d.name).sort((a, b) => a.localeCompare(b)), + ).toStrictEqual(['orders', 'payments']) + }) + + it('produces no output when --json flag is not provided', async () => { + await createGraph(ctx.testDir, { + version: '1.0', + metadata: { + sources: [{ repository: 'https://github.com/org/repo' }], + domains: { + orders: { + description: 'Order management', + systemType: 'domain', + }, + }, + }, + components: [], + links: [], + }) + + await createProgram().parseAsync(['node', 'riviere', 'query', 'domains']) + expect(ctx.consoleOutput).toHaveLength(0) + }) + }) + + describe('error handling', () => { + const ctx: TestContext = createTestContext() + setupCommandTest(ctx) + + it('returns GRAPH_NOT_FOUND when no graph exists', async () => { + await createProgram().parseAsync(['node', 'riviere', 'query', 'domains', '--json']) + expect(ctx.consoleOutput.join('\n')).toContain(CliErrorCode.GraphNotFound) + }) + }) +}) diff --git a/apps/cli/src/features/query/entrypoint/domains/entrypoint.ts b/apps/cli/src/features/query/entrypoint/domains/entrypoint.ts new file mode 100644 index 000000000..3a692e08e --- /dev/null +++ b/apps/cli/src/features/query/entrypoint/domains/entrypoint.ts @@ -0,0 +1,38 @@ +import { Command } from 'commander' +import { formatSuccess } from '../../../../infra/cli/presentation/output' +import { formatQueryGraphLoadFailure } from '../../../../infra/cli/presentation/query-graph-load-failure-output' +import { getDefaultGraphPathDescription } from '../../../../infra/cli/presentation/graph-path-option' +import type { ListDomains } from '@living-architecture/riviere-builder-use-cases/features/query/queries/list-domains' + +interface DomainsOptions { + graph?: string + json?: boolean +} + +/** @riviere-role cli-entrypoint */ +export function createDomainsCommand(listDomains: ListDomains): Command { + return new Command('domains') + .description('List domains with component counts') + .addHelpText( + 'after', + ` +Examples: + $ riviere query domains + $ riviere query domains --json +`, + ) + .option('--graph ', getDefaultGraphPathDescription()) + .option('--json', 'Output result as JSON') + .action(async (options: DomainsOptions) => { + const result = listDomains.execute({ graphPathOption: options.graph }) + + if ('kind' in result) { + console.log(JSON.stringify(formatQueryGraphLoadFailure(result))) + return + } + + if (options.json) { + console.log(JSON.stringify(formatSuccess(result))) + } + }) +} diff --git a/packages/riviere-cli/src/features/query/entrypoint/entry-points/entry-points.spec.ts b/apps/cli/src/features/query/entrypoint/entry-points/entry-points.spec.ts similarity index 94% rename from packages/riviere-cli/src/features/query/entrypoint/entry-points/entry-points.spec.ts rename to apps/cli/src/features/query/entrypoint/entry-points/entry-points.spec.ts index 262c2f5ef..0756a07d7 100644 --- a/packages/riviere-cli/src/features/query/entrypoint/entry-points/entry-points.spec.ts +++ b/apps/cli/src/features/query/entrypoint/entry-points/entry-points.spec.ts @@ -1,9 +1,7 @@ -import { - describe, it, expect -} from 'vitest' +import { describe, it, expect } from 'vitest' import { createProgram } from '../../../../shell/cli' -import { CliErrorCode } from '../../../../platform/infra/cli/presentation/error-codes' -import type { TestContext } from '../../../../platform/__fixtures__/command-test-fixtures' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' +import type { TestContext } from '../../../../__fixtures__/command-test-fixtures' import { createTestContext, setupCommandTest, @@ -14,7 +12,7 @@ import { useCaseComponent, eventHandlerComponent, TestAssertionError, -} from '../../../../platform/__fixtures__/command-test-fixtures' +} from '../../../../__fixtures__/command-test-fixtures' interface EntryPointsOutput { success: true diff --git a/apps/cli/src/features/query/entrypoint/entry-points/entrypoint.ts b/apps/cli/src/features/query/entrypoint/entry-points/entrypoint.ts new file mode 100644 index 000000000..221e80541 --- /dev/null +++ b/apps/cli/src/features/query/entrypoint/entry-points/entrypoint.ts @@ -0,0 +1,38 @@ +import { Command } from 'commander' +import { formatSuccess } from '../../../../infra/cli/presentation/output' +import { formatQueryGraphLoadFailure } from '../../../../infra/cli/presentation/query-graph-load-failure-output' +import { getDefaultGraphPathDescription } from '../../../../infra/cli/presentation/graph-path-option' +import type { ListEntryPoints } from '@living-architecture/riviere-builder-use-cases/features/query/queries/list-entry-points' + +interface EntryPointsOptions { + graph?: string + json?: boolean +} + +/** @riviere-role cli-entrypoint */ +export function createEntryPointsCommand(listEntryPoints: ListEntryPoints): Command { + return new Command('entry-points') + .description('List entry points (APIs, UIs, EventHandlers with no incoming links)') + .addHelpText( + 'after', + ` +Examples: + $ riviere query entry-points + $ riviere query entry-points --json +`, + ) + .option('--graph ', getDefaultGraphPathDescription()) + .option('--json', 'Output result as JSON') + .action(async (options: EntryPointsOptions) => { + const result = listEntryPoints.execute({ graphPathOption: options.graph }) + + if ('kind' in result) { + console.log(JSON.stringify(formatQueryGraphLoadFailure(result))) + return + } + + if (options.json) { + console.log(JSON.stringify(formatSuccess(result))) + } + }) +} diff --git a/apps/cli/src/features/query/entrypoint/orphans/entrypoint.ts b/apps/cli/src/features/query/entrypoint/orphans/entrypoint.ts new file mode 100644 index 000000000..ea0e1ad17 --- /dev/null +++ b/apps/cli/src/features/query/entrypoint/orphans/entrypoint.ts @@ -0,0 +1,38 @@ +import { Command } from 'commander' +import { formatSuccess } from '../../../../infra/cli/presentation/output' +import { formatQueryGraphLoadFailure } from '../../../../infra/cli/presentation/query-graph-load-failure-output' +import { getDefaultGraphPathDescription } from '../../../../infra/cli/presentation/graph-path-option' +import type { DetectOrphans } from '@living-architecture/riviere-builder-use-cases/features/query/queries/detect-orphans' + +interface OrphansOptions { + graph?: string + json?: boolean +} + +/** @riviere-role cli-entrypoint */ +export function createOrphansCommand(detectOrphans: DetectOrphans): Command { + return new Command('orphans') + .description('Find orphan components with no links') + .addHelpText( + 'after', + ` +Examples: + $ riviere query orphans + $ riviere query orphans --json +`, + ) + .option('--graph ', getDefaultGraphPathDescription()) + .option('--json', 'Output result as JSON') + .action(async (options: OrphansOptions) => { + const result = detectOrphans.execute({ graphPathOption: options.graph }) + + if ('kind' in result) { + console.log(JSON.stringify(formatQueryGraphLoadFailure(result))) + return + } + + if (options.json) { + console.log(JSON.stringify(formatSuccess(result))) + } + }) +} diff --git a/packages/riviere-cli/src/features/query/entrypoint/orphans/orphans.spec.ts b/apps/cli/src/features/query/entrypoint/orphans/orphans.spec.ts similarity index 94% rename from packages/riviere-cli/src/features/query/entrypoint/orphans/orphans.spec.ts rename to apps/cli/src/features/query/entrypoint/orphans/orphans.spec.ts index 3cba39957..696163e70 100644 --- a/packages/riviere-cli/src/features/query/entrypoint/orphans/orphans.spec.ts +++ b/apps/cli/src/features/query/entrypoint/orphans/orphans.spec.ts @@ -1,9 +1,7 @@ -import { - describe, it, expect -} from 'vitest' +import { describe, it, expect } from 'vitest' import { createProgram } from '../../../../shell/cli' -import { CliErrorCode } from '../../../../platform/infra/cli/presentation/error-codes' -import type { TestContext } from '../../../../platform/__fixtures__/command-test-fixtures' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' +import type { TestContext } from '../../../../__fixtures__/command-test-fixtures' import { createTestContext, setupCommandTest, @@ -12,7 +10,7 @@ import { apiComponent, useCaseComponent, TestAssertionError, -} from '../../../../platform/__fixtures__/command-test-fixtures' +} from '../../../../__fixtures__/command-test-fixtures' interface OrphansSuccessOutput { success: true diff --git a/apps/cli/src/features/query/entrypoint/search/entrypoint.ts b/apps/cli/src/features/query/entrypoint/search/entrypoint.ts new file mode 100644 index 000000000..351ca9bdf --- /dev/null +++ b/apps/cli/src/features/query/entrypoint/search/entrypoint.ts @@ -0,0 +1,45 @@ +import { Command } from 'commander' +import { formatSuccess } from '../../../../infra/cli/presentation/output' +import { formatQueryGraphLoadFailure } from '../../../../infra/cli/presentation/query-graph-load-failure-output' +import { getDefaultGraphPathDescription } from '../../../../infra/cli/presentation/graph-path-option' +import { toComponentOutput } from '../_platform/cli/component-output' +import type { SearchComponents } from '@living-architecture/riviere-builder-use-cases/features/query/queries/search-components' + +interface SearchOptions { + graph?: string + json?: boolean +} + +/** @riviere-role cli-entrypoint */ +export function createSearchCommand(searchComponents: SearchComponents): Command { + return new Command('search') + .description('Search components by name') + .addHelpText( + 'after', + ` +Examples: + $ riviere query search order + $ riviere query search "place-order" --json +`, + ) + .argument('', 'Search term') + .option('--graph ', getDefaultGraphPathDescription()) + .option('--json', 'Output result as JSON') + .action(async (term: string, options: SearchOptions) => { + const result = searchComponents.execute({ + graphPathOption: options.graph, + term, + }) + + if ('kind' in result) { + console.log(JSON.stringify(formatQueryGraphLoadFailure(result))) + return + } + + const components = result.components.map(toComponentOutput) + + if (options.json) { + console.log(JSON.stringify(formatSuccess({ components }))) + } + }) +} diff --git a/packages/riviere-cli/src/features/query/entrypoint/search/search.spec.ts b/apps/cli/src/features/query/entrypoint/search/search.spec.ts similarity index 95% rename from packages/riviere-cli/src/features/query/entrypoint/search/search.spec.ts rename to apps/cli/src/features/query/entrypoint/search/search.spec.ts index 2c92275ce..bbd2befc5 100644 --- a/packages/riviere-cli/src/features/query/entrypoint/search/search.spec.ts +++ b/apps/cli/src/features/query/entrypoint/search/search.spec.ts @@ -1,16 +1,14 @@ -import { - describe, it, expect -} from 'vitest' +import { describe, it, expect } from 'vitest' import { createProgram } from '../../../../shell/cli' -import { CliErrorCode } from '../../../../platform/infra/cli/presentation/error-codes' -import type { TestContext } from '../../../../platform/__fixtures__/command-test-fixtures' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' +import type { TestContext } from '../../../../__fixtures__/command-test-fixtures' import { createTestContext, setupCommandTest, createGraph, sourceLocation, TestAssertionError, -} from '../../../../platform/__fixtures__/command-test-fixtures' +} from '../../../../__fixtures__/command-test-fixtures' interface ComponentInfo { id: string diff --git a/apps/cli/src/features/query/entrypoint/trace/entrypoint.ts b/apps/cli/src/features/query/entrypoint/trace/entrypoint.ts new file mode 100644 index 000000000..012fdec67 --- /dev/null +++ b/apps/cli/src/features/query/entrypoint/trace/entrypoint.ts @@ -0,0 +1,52 @@ +import { Command } from 'commander' +import { formatError, formatSuccess } from '../../../../infra/cli/presentation/output' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' +import { formatQueryGraphLoadFailure } from '../../../../infra/cli/presentation/query-graph-load-failure-output' +import { getDefaultGraphPathDescription } from '../../../../infra/cli/presentation/graph-path-option' +import type { TraceFlow } from '@living-architecture/riviere-builder-use-cases/features/query/queries/trace-flow' + +interface TraceOptions { + graph?: string + json?: boolean +} + +/** @riviere-role cli-entrypoint */ +export function createTraceCommand(traceFlow: TraceFlow): Command { + return new Command('trace') + .description('Trace flow from a component (bidirectional)') + .addHelpText( + 'after', + ` +Examples: + $ riviere query trace "orders:api:api:postorders" + $ riviere query trace "orders:checkout:usecase:placeorder" --json +`, + ) + .argument('', 'Component ID to trace from') + .option('--graph ', getDefaultGraphPathDescription()) + .option('--json', 'Output result as JSON') + .action(async (componentIdArg: string, options: TraceOptions) => { + const result = traceFlow.execute({ + componentId: componentIdArg, + graphPathOption: options.graph, + }) + + if ('kind' in result) { + console.log(JSON.stringify(formatQueryGraphLoadFailure(result))) + return + } + + if (!result.success) { + console.log( + JSON.stringify( + formatError(CliErrorCode.ComponentNotFound, result.message, result.suggestions), + ), + ) + return + } + + if (options.json) { + console.log(JSON.stringify(formatSuccess(result.flow))) + } + }) +} diff --git a/packages/riviere-cli/src/features/query/entrypoint/trace/trace.spec.ts b/apps/cli/src/features/query/entrypoint/trace/trace.spec.ts similarity index 83% rename from packages/riviere-cli/src/features/query/entrypoint/trace/trace.spec.ts rename to apps/cli/src/features/query/entrypoint/trace/trace.spec.ts index 8fd3d528e..2a2084c91 100644 --- a/packages/riviere-cli/src/features/query/entrypoint/trace/trace.spec.ts +++ b/apps/cli/src/features/query/entrypoint/trace/trace.spec.ts @@ -1,9 +1,7 @@ -import { - describe, it, expect -} from 'vitest' +import { describe, it, expect } from 'vitest' import { createProgram } from '../../../../shell/cli' -import { CliErrorCode } from '../../../../platform/infra/cli/presentation/error-codes' -import type { TestContext } from '../../../../platform/__fixtures__/command-test-fixtures' +import { CliErrorCode } from '../../../../infra/cli/presentation/error-codes' +import type { TestContext } from '../../../../__fixtures__/command-test-fixtures' import { createTestContext, setupCommandTest, @@ -12,7 +10,7 @@ import { apiComponent, useCaseComponent, TestAssertionError, -} from '../../../../platform/__fixtures__/command-test-fixtures' +} from '../../../../__fixtures__/command-test-fixtures' interface TraceSuccessOutput { success: true @@ -205,37 +203,5 @@ describe('riviere query trace', () => { expect(output.error.code).toBe(CliErrorCode.ComponentNotFound) expect(output.error.message).toContain('orders:checkout:api:nonexistent') }) - - it('propagates unexpected errors thrown by traceFlow', async () => { - await createGraph(ctx.testDir, { - version: '1.0', - metadata: baseMetadata, - components: [apiComponent], - links: [], - }) - - const queryModule = await import('@living-architecture/riviere-query') - const queryClass = queryModule.RiviereQuery - const originalTraceFlow = queryClass.prototype.traceFlow - - queryClass.prototype.traceFlow = () => { - throw new TestAssertionError('Unexpected internal error') - } - - try { - await expect( - createProgram().parseAsync([ - 'node', - 'riviere', - 'query', - 'trace', - 'orders:checkout:api:place-order', - '--json', - ]), - ).rejects.toThrow('Unexpected internal error') - } finally { - queryClass.prototype.traceFlow = originalTraceFlow - } - }) }) }) diff --git a/apps/cli/src/features/role-enforcement/entrypoint/role-enforcement/entrypoint.ts b/apps/cli/src/features/role-enforcement/entrypoint/role-enforcement/entrypoint.ts new file mode 100644 index 000000000..2596a3ba9 --- /dev/null +++ b/apps/cli/src/features/role-enforcement/entrypoint/role-enforcement/entrypoint.ts @@ -0,0 +1,23 @@ +import { RunRoleEnforcement } from '@living-architecture/riviere-role-enforcement-use-cases' + +/** @riviere-role cli-entrypoint */ +export function main( + application: RunRoleEnforcement, + configModulePath: string, + configDir: string, + packageFilter?: string, +): number { + const result = application.execute({ + configDir, + configModulePath, + ...(packageFilter === undefined ? {} : { packageFilter }), + }) + if (result.stdout !== '') { + process.stdout.write(result.stdout) + } + if (result.stderr !== '') { + process.stderr.write(result.stderr) + } + process.stderr.write(`Role enforcement completed in ${Math.round(result.durationMs)}ms\n`) + return result.exitCode +} diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts new file mode 100644 index 000000000..dd303c160 --- /dev/null +++ b/apps/cli/src/index.ts @@ -0,0 +1,3 @@ +export { createProgram } from './shell/cli' +export { CliErrorCode, ConfigValidationError, ExitCode } from './infra/cli/presentation/error-codes' +export { formatError, formatSuccess } from './infra/cli/presentation/output' diff --git a/packages/riviere-cli/src/platform/infra/cli/presentation/add-component-hints.ts b/apps/cli/src/infra/cli/presentation/add-component-hints.ts similarity index 100% rename from packages/riviere-cli/src/platform/infra/cli/presentation/add-component-hints.ts rename to apps/cli/src/infra/cli/presentation/add-component-hints.ts diff --git a/packages/riviere-cli/src/platform/infra/cli/presentation/error-codes.spec.ts b/apps/cli/src/infra/cli/presentation/error-codes.spec.ts similarity index 93% rename from packages/riviere-cli/src/platform/infra/cli/presentation/error-codes.spec.ts rename to apps/cli/src/infra/cli/presentation/error-codes.spec.ts index 24e91e532..bd61ccd6c 100644 --- a/packages/riviere-cli/src/platform/infra/cli/presentation/error-codes.spec.ts +++ b/apps/cli/src/infra/cli/presentation/error-codes.spec.ts @@ -1,6 +1,4 @@ -import { - CliErrorCode, ExitCode -} from './error-codes' +import { CliErrorCode, ExitCode } from './error-codes' describe('CliErrorCode', () => { it.each([ diff --git a/packages/riviere-cli/src/platform/infra/cli/presentation/error-codes.ts b/apps/cli/src/infra/cli/presentation/error-codes.ts similarity index 83% rename from packages/riviere-cli/src/platform/infra/cli/presentation/error-codes.ts rename to apps/cli/src/infra/cli/presentation/error-codes.ts index 4d3cc9271..ee73c98d6 100644 --- a/packages/riviere-cli/src/platform/infra/cli/presentation/error-codes.ts +++ b/apps/cli/src/infra/cli/presentation/error-codes.ts @@ -7,8 +7,15 @@ export enum ExitCode { /** @riviere-role cli-error */ export class ConfigValidationError extends Error { + /** Stable CLI error code for presentation and exit handling. */ readonly errorCode: CliErrorCode + /** + * Creates a configuration validation error. + * + * @param code - Stable CLI error code + * @param message - Human-readable validation failure + */ constructor(code: CliErrorCode, message: string) { super(message) this.name = 'ConfigValidationError' diff --git a/apps/cli/src/infra/cli/presentation/exit-with-cli-error.ts b/apps/cli/src/infra/cli/presentation/exit-with-cli-error.ts new file mode 100644 index 000000000..d0ebae330 --- /dev/null +++ b/apps/cli/src/infra/cli/presentation/exit-with-cli-error.ts @@ -0,0 +1,13 @@ +import { type CliErrorCode, ExitCode } from './error-codes' +import { formatError } from './output' + +/** @riviere-role cli-response-writer */ +export function exitWithCliError( + code: CliErrorCode, + message: string, + exitCode: ExitCode, + suggestions: string[], +): never { + console.log(JSON.stringify(formatError(code, message, suggestions))) + process.exit(exitCode) +} diff --git a/packages/riviere-cli/src/platform/infra/cli/presentation/format-pr-markdown.spec.ts b/apps/cli/src/infra/cli/presentation/format-pr-markdown.spec.ts similarity index 98% rename from packages/riviere-cli/src/platform/infra/cli/presentation/format-pr-markdown.spec.ts rename to apps/cli/src/infra/cli/presentation/format-pr-markdown.spec.ts index 632051b2b..1afdac79a 100644 --- a/packages/riviere-cli/src/platform/infra/cli/presentation/format-pr-markdown.spec.ts +++ b/apps/cli/src/infra/cli/presentation/format-pr-markdown.spec.ts @@ -1,6 +1,4 @@ -import { - describe, it, expect -} from 'vitest' +import { describe, it, expect } from 'vitest' import { formatPrMarkdown } from './format-pr-markdown' import type { CategorizedComponents } from './format-pr-markdown' diff --git a/packages/riviere-cli/src/platform/infra/cli/presentation/format-pr-markdown.ts b/apps/cli/src/infra/cli/presentation/format-pr-markdown.ts similarity index 100% rename from packages/riviere-cli/src/platform/infra/cli/presentation/format-pr-markdown.ts rename to apps/cli/src/infra/cli/presentation/format-pr-markdown.ts diff --git a/packages/riviere-cli/src/platform/infra/cli/presentation/graph-path-option.ts b/apps/cli/src/infra/cli/presentation/graph-path-option.ts similarity index 100% rename from packages/riviere-cli/src/platform/infra/cli/presentation/graph-path-option.ts rename to apps/cli/src/infra/cli/presentation/graph-path-option.ts diff --git a/apps/cli/src/infra/cli/presentation/output-writer.ts b/apps/cli/src/infra/cli/presentation/output-writer.ts new file mode 100644 index 000000000..6c9df098a --- /dev/null +++ b/apps/cli/src/infra/cli/presentation/output-writer.ts @@ -0,0 +1,32 @@ +import { writeFileSync } from 'node:fs' +import { formatError, formatSuccess } from './output' +import { CliErrorCode, ExitCode } from './error-codes' + +interface OutputOptions { + output?: string +} + +/** @riviere-role cli-response-writer */ +export function outputResult( + data: ReturnType>, + options: OutputOptions, +): void { + if (options.output !== undefined) { + try { + writeFileSync(options.output, JSON.stringify(data)) + } catch { + console.log( + JSON.stringify( + formatError( + CliErrorCode.ValidationError, + 'Failed to write output file: ' + options.output, + ), + ), + ) + process.exit(ExitCode.RuntimeError) + } + return + } + + console.log(JSON.stringify(data)) +} diff --git a/packages/riviere-cli/src/platform/infra/cli/presentation/output.spec.ts b/apps/cli/src/infra/cli/presentation/output.spec.ts similarity index 94% rename from packages/riviere-cli/src/platform/infra/cli/presentation/output.spec.ts rename to apps/cli/src/infra/cli/presentation/output.spec.ts index 9d526eb3a..4d3183434 100644 --- a/packages/riviere-cli/src/platform/infra/cli/presentation/output.spec.ts +++ b/apps/cli/src/infra/cli/presentation/output.spec.ts @@ -1,6 +1,4 @@ -import { - formatSuccess, formatError, type SuccessOutput, type ErrorOutput -} from './output' +import { formatSuccess, formatError, type SuccessOutput, type ErrorOutput } from './output' import { CliErrorCode } from './error-codes' describe('formatSuccess', () => { diff --git a/packages/riviere-cli/src/platform/infra/cli/presentation/output.ts b/apps/cli/src/infra/cli/presentation/output.ts similarity index 100% rename from packages/riviere-cli/src/platform/infra/cli/presentation/output.ts rename to apps/cli/src/infra/cli/presentation/output.ts diff --git a/packages/riviere-cli/src/platform/infra/cli/presentation/query-graph-load-failure-output.spec.ts b/apps/cli/src/infra/cli/presentation/query-graph-load-failure-output.spec.ts similarity index 95% rename from packages/riviere-cli/src/platform/infra/cli/presentation/query-graph-load-failure-output.spec.ts rename to apps/cli/src/infra/cli/presentation/query-graph-load-failure-output.spec.ts index c4a41ef2a..cd7bd17dc 100644 --- a/packages/riviere-cli/src/platform/infra/cli/presentation/query-graph-load-failure-output.spec.ts +++ b/apps/cli/src/infra/cli/presentation/query-graph-load-failure-output.spec.ts @@ -1,6 +1,4 @@ -import { - describe, expect, it -} from 'vitest' +import { describe, expect, it } from 'vitest' import { CliErrorCode } from './error-codes' import { formatQueryGraphLoadFailure } from './query-graph-load-failure-output' diff --git a/packages/riviere-cli/src/platform/infra/cli/presentation/query-graph-load-failure-output.ts b/apps/cli/src/infra/cli/presentation/query-graph-load-failure-output.ts similarity index 100% rename from packages/riviere-cli/src/platform/infra/cli/presentation/query-graph-load-failure-output.ts rename to apps/cli/src/infra/cli/presentation/query-graph-load-failure-output.ts diff --git a/packages/riviere-cli/src/shell/bin.ts b/apps/cli/src/shell/bin.ts similarity index 100% rename from packages/riviere-cli/src/shell/bin.ts rename to apps/cli/src/shell/bin.ts diff --git a/packages/riviere-cli/src/shell/cli.package-json.spec.ts b/apps/cli/src/shell/cli.package-json.spec.ts similarity index 93% rename from packages/riviere-cli/src/shell/cli.package-json.spec.ts rename to apps/cli/src/shell/cli.package-json.spec.ts index 17d0a2818..fa8d428a3 100644 --- a/packages/riviere-cli/src/shell/cli.package-json.spec.ts +++ b/apps/cli/src/shell/cli.package-json.spec.ts @@ -1,6 +1,4 @@ -import { - afterEach, describe, expect, it, vi -} from 'vitest' +import { afterEach, describe, expect, it, vi } from 'vitest' async function importCliWithPackageJson(packageJson: unknown): Promise { vi.resetModules() diff --git a/packages/riviere-cli/src/shell/cli.spec.ts b/apps/cli/src/shell/cli.spec.ts similarity index 97% rename from packages/riviere-cli/src/shell/cli.spec.ts rename to apps/cli/src/shell/cli.spec.ts index 0aabd40b8..e2b1bf91f 100644 --- a/packages/riviere-cli/src/shell/cli.spec.ts +++ b/apps/cli/src/shell/cli.spec.ts @@ -1,7 +1,5 @@ import { Command } from 'commander' -import { - describe, expect, it, vi -} from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { createProgram } from './cli' describe('createProgram', () => { diff --git a/apps/cli/src/shell/cli.ts b/apps/cli/src/shell/cli.ts new file mode 100644 index 000000000..27c1d8282 --- /dev/null +++ b/apps/cli/src/shell/cli.ts @@ -0,0 +1,143 @@ +import { Command } from 'commander' +import { createRequire } from 'module' +import { AddComponent } from '@living-architecture/riviere-builder-use-cases/features/builder/commands/add-component' +import { AddDomain } from '@living-architecture/riviere-builder-use-cases/features/builder/commands/add-domain' +import { AddSource } from '@living-architecture/riviere-builder-use-cases/features/builder/commands/add-source' +import { CheckConsistency } from '@living-architecture/riviere-builder-use-cases/features/builder/commands/check-consistency' +import { ComponentChecklist } from '@living-architecture/riviere-builder-use-cases/features/builder/commands/component-checklist' +import { ComponentSummary } from '@living-architecture/riviere-builder-use-cases/features/builder/commands/component-summary' +import { DefineCustomType } from '@living-architecture/riviere-builder-use-cases/features/builder/commands/define-custom-type' +import { DefineRelationshipType } from '@living-architecture/riviere-builder-use-cases/features/builder/commands/define-relationship-type' +import { EnrichComponent } from '@living-architecture/riviere-builder-use-cases/features/builder/commands/enrich-component' +import { FinalizeGraph } from '@living-architecture/riviere-builder-use-cases/features/builder/commands/finalize-graph' +import { InitGraph } from '@living-architecture/riviere-builder-use-cases/features/builder/commands/init-graph' +import { LinkComponents } from '@living-architecture/riviere-builder-use-cases/features/builder/commands/link-components' +import { LinkExternal } from '@living-architecture/riviere-builder-use-cases/features/builder/commands/link-external' +import { LinkHttp } from '@living-architecture/riviere-builder-use-cases/features/builder/commands/link-http' +import { ValidateGraph } from '@living-architecture/riviere-builder-use-cases/features/builder/commands/validate-graph' +import { RiviereBuilderRepository } from '@living-architecture/riviere-builder-use-cases/features/builder/data-access/riviere-builder/riviere-builder-repository' +import { createAddComponentCommand } from '../features/builder/entrypoint/add-component/entrypoint' +import { createAddDomainCommand } from '../features/builder/entrypoint/add-domain/entrypoint' +import { createAddSourceCommand } from '../features/builder/entrypoint/add-source/entrypoint' +import { createCheckConsistencyCommand } from '../features/builder/entrypoint/check-consistency/entrypoint' +import { createComponentChecklistCommand } from '../features/builder/entrypoint/component-checklist/entrypoint' +import { createComponentSummaryCommand } from '../features/builder/entrypoint/component-summary/entrypoint' +import { createDefineCustomTypeCommand } from '../features/builder/entrypoint/define-custom-type/entrypoint' +import { createDefineRelationshipTypeCommand } from '../features/builder/entrypoint/define-relationship-type/entrypoint' +import { createEnrichCommand } from '../features/builder/entrypoint/enrich/entrypoint' +import { createFinalizeCommand } from '../features/builder/entrypoint/finalize/entrypoint' +import { createInitCommand } from '../features/builder/entrypoint/init/entrypoint' +import { createLinkCommand } from '../features/builder/entrypoint/link/entrypoint' +import { createLinkExternalCommand } from '../features/builder/entrypoint/link-external/entrypoint' +import { createLinkHttpCommand } from '../features/builder/entrypoint/link-http/entrypoint' +import { createValidateCommand } from '../features/builder/entrypoint/validate/entrypoint' +import { EnrichDraftComponents } from '@living-architecture/riviere-extract-ts-use-cases/features/extract/commands/enrich-draft-components' +import { ExtractDraftComponents } from '@living-architecture/riviere-extract-ts-use-cases/features/extract/commands/extract-draft-components' +import { ExtractionProjectRepository } from '@living-architecture/riviere-extract-ts-use-cases/features/extract/data-access/extraction-project/extraction-project-repository' +import { createExtractCommand } from '../features/extract/entrypoint/extract/entrypoint' +import { DetectOrphans } from '@living-architecture/riviere-builder-use-cases/features/query/queries/detect-orphans' +import { ListComponents } from '@living-architecture/riviere-builder-use-cases/features/query/queries/list-components' +import { ListDomains } from '@living-architecture/riviere-builder-use-cases/features/query/queries/list-domains' +import { ListEntryPoints } from '@living-architecture/riviere-builder-use-cases/features/query/queries/list-entry-points' +import { SearchComponents } from '@living-architecture/riviere-builder-use-cases/features/query/queries/search-components' +import { TraceFlow } from '@living-architecture/riviere-builder-use-cases/features/query/queries/trace-flow' +import { + ComponentListLoader, + ComponentSearchLoader, + DomainListLoader, + EntryPointListLoader, + FlowTraceLoader, + OrphanListLoader, +} from '@living-architecture/riviere-builder-use-cases/features/query/data-access/graph/query-loaders' +import { createComponentsCommand } from '../features/query/entrypoint/components/entrypoint' +import { createDomainsCommand } from '../features/query/entrypoint/domains/entrypoint' +import { createEntryPointsCommand } from '../features/query/entrypoint/entry-points/entrypoint' +import { createOrphansCommand } from '../features/query/entrypoint/orphans/entrypoint' +import { createSearchCommand } from '../features/query/entrypoint/search/entrypoint' +import { createTraceCommand } from '../features/query/entrypoint/trace/entrypoint' + +interface PackageJson { + version: string +} + +class InvalidPackageJsonError extends Error { + constructor(reason: string) { + super(`Invalid package.json: ${reason}`) + this.name = 'InvalidPackageJsonError' + } +} + +function parsePackageJson(pkg: unknown): PackageJson { + if (typeof pkg !== 'object' || pkg === null || !('version' in pkg)) { + throw new InvalidPackageJsonError('missing version field') + } + if (typeof pkg.version !== 'string') { + throw new InvalidPackageJsonError('version must be a string') + } + return { version: pkg.version } +} + +declare const INJECTED_VERSION: string | undefined + +function loadPackageJson(): PackageJson { + if (typeof INJECTED_VERSION === 'string') { + return { version: INJECTED_VERSION } + } + const require = createRequire(import.meta.url) + return parsePackageJson(require('../../package.json')) +} + +const packageJson = loadPackageJson() + +/** + * Wires the CLI entrypoints to their use cases and adapters. + * + * @riviere-role main + * @returns Configured Rivière CLI program + */ +export function createProgram(): Command { + const builderRepository = new RiviereBuilderRepository() + const extractionProjectRepository = new ExtractionProjectRepository() + + const program = new Command() + + program.name('riviere').version(packageJson.version) + + const builderCmd = program.command('builder').description('Commands for building a graph') + + builderCmd.addCommand(createAddComponentCommand(new AddComponent(builderRepository))) + builderCmd.addCommand(createAddDomainCommand(new AddDomain(builderRepository))) + builderCmd.addCommand(createAddSourceCommand(new AddSource(builderRepository))) + builderCmd.addCommand(createInitCommand(new InitGraph(builderRepository))) + builderCmd.addCommand(createLinkCommand(new LinkComponents(builderRepository))) + builderCmd.addCommand(createLinkExternalCommand(new LinkExternal(builderRepository))) + builderCmd.addCommand(createLinkHttpCommand(new LinkHttp(builderRepository))) + builderCmd.addCommand(createValidateCommand(new ValidateGraph(builderRepository))) + builderCmd.addCommand(createFinalizeCommand(new FinalizeGraph(builderRepository))) + builderCmd.addCommand(createEnrichCommand(new EnrichComponent(builderRepository))) + builderCmd.addCommand(createComponentSummaryCommand(new ComponentSummary(builderRepository))) + builderCmd.addCommand(createComponentChecklistCommand(new ComponentChecklist(builderRepository))) + builderCmd.addCommand(createCheckConsistencyCommand(new CheckConsistency(builderRepository))) + builderCmd.addCommand(createDefineCustomTypeCommand(new DefineCustomType(builderRepository))) + builderCmd.addCommand( + createDefineRelationshipTypeCommand(new DefineRelationshipType(builderRepository)), + ) + + const queryCmd = program.command('query').description('Commands for querying a graph') + + queryCmd.addCommand(createEntryPointsCommand(new ListEntryPoints(new EntryPointListLoader()))) + queryCmd.addCommand(createDomainsCommand(new ListDomains(new DomainListLoader()))) + queryCmd.addCommand(createTraceCommand(new TraceFlow(new FlowTraceLoader()))) + queryCmd.addCommand(createOrphansCommand(new DetectOrphans(new OrphanListLoader()))) + queryCmd.addCommand(createComponentsCommand(new ListComponents(new ComponentListLoader()))) + queryCmd.addCommand(createSearchCommand(new SearchComponents(new ComponentSearchLoader()))) + + program.addCommand( + createExtractCommand( + new ExtractDraftComponents(extractionProjectRepository), + new EnrichDraftComponents(extractionProjectRepository), + ), + ) + + return program +} diff --git a/apps/cli/src/shell/global-error-handler.spec.ts b/apps/cli/src/shell/global-error-handler.spec.ts new file mode 100644 index 000000000..e3679bd1b --- /dev/null +++ b/apps/cli/src/shell/global-error-handler.spec.ts @@ -0,0 +1,55 @@ +import { describe, it, expect } from 'vitest' +import { handleGlobalError } from './global-error-handler' +import { + CliErrorCode, + ConfigValidationError, + ExitCode, +} from '../infra/cli/presentation/error-codes' +import { + TestAssertionError, + createTestContext, + setupCommandTest, +} from '../__fixtures__/command-test-fixtures' +import type { TestContext } from '../__fixtures__/command-test-fixtures' + +function firstConsoleOutput(consoleOutput: string[]): unknown { + const first = consoleOutput[0] + if (first === undefined) { + throw new TestAssertionError('Expected console output but got empty array') + } + return JSON.parse(first) +} + +describe('handleGlobalError', () => { + const ctx: TestContext = createTestContext() + setupCommandTest(ctx) + + it('formats ConfigValidationError with config validation exit code', () => { + const error = new ConfigValidationError(CliErrorCode.ConfigNotFound, 'Config file not found') + + expect(() => handleGlobalError(error)).toThrow('process.exit') + + const output = firstConsoleOutput(ctx.consoleOutput) + expect(output).toMatchObject({ error: { code: CliErrorCode.ConfigNotFound } }) + expect(process.exit).toHaveBeenCalledWith(ExitCode.ConfigValidation) + }) + + it('formats ConfigValidationError for missing files as validation error', () => { + const error = new ConfigValidationError( + CliErrorCode.ValidationError, + 'Files not found: missing.ts', + ) + + expect(() => handleGlobalError(error)).toThrow('process.exit') + + const output = firstConsoleOutput(ctx.consoleOutput) + expect(output).toMatchObject({ error: { code: CliErrorCode.ValidationError } }) + expect(process.exit).toHaveBeenCalledWith(ExitCode.ConfigValidation) + }) + + it('re-throws unknown errors', () => { + const error = new TestAssertionError('unexpected') + + expect(() => handleGlobalError(error)).toThrow('unexpected') + }) +}) diff --git a/apps/cli/src/shell/global-error-handler.ts b/apps/cli/src/shell/global-error-handler.ts new file mode 100644 index 000000000..1148f4d3a --- /dev/null +++ b/apps/cli/src/shell/global-error-handler.ts @@ -0,0 +1,12 @@ +import { formatError } from '../infra/cli/presentation/output' +import { ExitCode, ConfigValidationError } from '../infra/cli/presentation/error-codes' + +/** @riviere-role cli-error-handler */ +export function handleGlobalError(error: unknown): never { + if (error instanceof ConfigValidationError) { + console.log(JSON.stringify(formatError(error.errorCode, error.message))) + process.exit(ExitCode.ConfigValidation) + } + + throw error +} diff --git a/apps/cli/src/shell/release-configuration.spec.ts b/apps/cli/src/shell/release-configuration.spec.ts new file mode 100644 index 000000000..d67d0a381 --- /dev/null +++ b/apps/cli/src/shell/release-configuration.spec.ts @@ -0,0 +1,97 @@ +import { existsSync, readdirSync, readFileSync } from 'node:fs' +import path, { dirname } from 'node:path' +import { fileURLToPath } from 'node:url' +import { describe, expect, it } from 'vitest' +import { z } from 'zod' + +const repoRoot = path.resolve(dirname(fileURLToPath(import.meta.url)), '../../../..') +const riviereProjectSelectors = ['riviere-*', '@living-architecture/riviere-*'] + +const packageManifestSchema = z.object({ + name: z.string(), + private: z.boolean().optional(), + publishConfig: z.object({ access: z.literal('public') }).optional(), +}) +const nxConfigurationSchema = z.object({ + release: z.object({ + projects: z.array(z.string()), + version: z.object({ preVersionCommand: z.string() }), + }), +}) + +describe('release configuration', () => { + it('builds every public Rivière package before Nx versions and releases it', () => { + const nxConfiguration = readJson('nx.json', nxConfigurationSchema) + const publicRivierePackages = readWorkspacePackageManifests().filter( + (manifest) => manifest.private !== true && manifest.name.includes('riviere'), + ) + + expect(nxConfiguration.release.projects).toStrictEqual(riviereProjectSelectors) + expect(nxConfiguration.release.version.preVersionCommand).toContain( + `--projects=${riviereProjectSelectors.join(',')}`, + ) + + for (const manifest of publicRivierePackages) { + expect(manifest.publishConfig).toStrictEqual({ access: 'public' }) + expect(riviereProjectSelectors.some((selector) => matches(selector, manifest.name))).toBe( + true, + ) + } + }) + + it('names every subdomain package after its subdomain and package type', () => { + for (const subdomain of readdirSync(path.join(repoRoot, 'packages'), { + withFileTypes: true, + })) { + if (!subdomain.isDirectory()) continue + + const subdomainPath = path.join(repoRoot, 'packages', subdomain.name) + for (const packageType of readdirSync(subdomainPath, { withFileTypes: true })) { + if ( + !packageType.isDirectory() || + !existsSync(path.join(subdomainPath, packageType.name, 'package.json')) + ) { + continue + } + + const manifest = readJson( + path.join('packages', subdomain.name, packageType.name, 'package.json'), + packageManifestSchema, + ) + expect(manifest.name).toBe(`@living-architecture/${subdomain.name}-${packageType.name}`) + } + } + }) +}) + +function readWorkspacePackageManifests(): z.infer[] { + return [ + readJson('apps/cli/package.json', packageManifestSchema), + ...readdirSync(path.join(repoRoot, 'packages'), { withFileTypes: true }).flatMap( + (subdomain) => { + if (!subdomain.isDirectory()) return [] + const subdomainPath = path.join(repoRoot, 'packages', subdomain.name) + return readdirSync(subdomainPath, { withFileTypes: true }) + .filter( + (entry) => + entry.isDirectory() && + existsSync(path.join(subdomainPath, entry.name, 'package.json')), + ) + .map((entry) => + readJson( + path.join('packages', subdomain.name, entry.name, 'package.json'), + packageManifestSchema, + ), + ) + }, + ), + ] +} + +function readJson(relativePath: string, schema: z.ZodType): T { + return schema.parse(JSON.parse(readFileSync(path.join(repoRoot, relativePath), 'utf8'))) +} + +function matches(selector: string, projectName: string): boolean { + return selector.endsWith('*') && projectName.startsWith(selector.slice(0, -1)) +} diff --git a/apps/cli/src/shell/role-enforcement-bin.ts b/apps/cli/src/shell/role-enforcement-bin.ts new file mode 100644 index 000000000..897e34854 --- /dev/null +++ b/apps/cli/src/shell/role-enforcement-bin.ts @@ -0,0 +1,44 @@ +import path from 'node:path' +import { performance } from 'node:perf_hooks' +import { fileURLToPath } from 'node:url' +import { + createOxlintRoleEnforcementRunner, + RoleEnforcementProjectRepository, + RunRoleEnforcement, + runOxlint, +} from '@living-architecture/riviere-role-enforcement-use-cases' +import { main } from '../features/role-enforcement/entrypoint/role-enforcement/entrypoint' + +const configModulePath = process.argv[2] +if (configModulePath === undefined) { + process.stderr.write( + 'Usage: riviere-role-enforcement [--package ]\n', + ) + process.exitCode = 1 +} else { + const packageFilter = readPackageFilter(process.argv) + const absolutePath = path.resolve(configModulePath) + const pluginPath = fileURLToPath( + import.meta.resolve('@living-architecture/riviere-role-enforcement-domain-model/plugin'), + ) + const application = new RunRoleEnforcement({ + now: () => performance.now(), + projectRepository: new RoleEnforcementProjectRepository(), + runner: createOxlintRoleEnforcementRunner(runOxlint, pluginPath), + }) + process.exitCode = main(application, absolutePath, process.cwd(), packageFilter) +} + +function readPackageFilter(argv: readonly string[]): string | undefined { + const flagIndex = argv.indexOf('--package') + if (flagIndex < 0) { + return undefined + } + const value = argv[flagIndex + 1] + if (value === undefined) { + process.stderr.write('Error: --package requires a value\n') + process.exitCode = 1 + return undefined + } + return value +} diff --git a/packages/riviere-cli/tsconfig.json b/apps/cli/tsconfig.json similarity index 100% rename from packages/riviere-cli/tsconfig.json rename to apps/cli/tsconfig.json diff --git a/apps/cli/tsconfig.lib.json b/apps/cli/tsconfig.lib.json new file mode 100644 index 000000000..2f27e148a --- /dev/null +++ b/apps/cli/tsconfig.lib.json @@ -0,0 +1,48 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": ".", + "rootDir": "src", + "outDir": "dist", + "tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo", + "emitDeclarationOnly": false, + "forceConsistentCasingInFileNames": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "references": [ + { + "path": "../../packages/riviere-schema/published-language/tsconfig.lib.json" + }, + { + "path": "../../packages/riviere-role-enforcement/use-cases/tsconfig.lib.json" + }, + { + "path": "../../packages/riviere-role-enforcement/domain-model/tsconfig.lib.json" + }, + { + "path": "../../packages/riviere-extract-ts/use-cases/tsconfig.lib.json" + }, + { + "path": "../../packages/riviere-extract-ts/domain-model/tsconfig.lib.json" + }, + { + "path": "../../packages/riviere-builder/use-cases/tsconfig.lib.json" + } + ], + "exclude": [ + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/__fixtures__/**" + ] +} diff --git a/packages/riviere-cli/tsconfig.scripts.json b/apps/cli/tsconfig.scripts.json similarity index 100% rename from packages/riviere-cli/tsconfig.scripts.json rename to apps/cli/tsconfig.scripts.json diff --git a/apps/cli/tsconfig.spec.json b/apps/cli/tsconfig.spec.json new file mode 100644 index 000000000..7dd4df0d6 --- /dev/null +++ b/apps/cli/tsconfig.spec.json @@ -0,0 +1,29 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./out-tsc/vitest", + "types": ["vitest/globals", "vitest/importMeta", "vite/client", "node", "vitest"], + "forceConsistentCasingInFileNames": true + }, + "include": [ + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts", + "src/**/__fixtures__/**/*.ts" + ], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/packages/riviere-cli/vite.config.ts b/apps/cli/vite.config.ts similarity index 100% rename from packages/riviere-cli/vite.config.ts rename to apps/cli/vite.config.ts diff --git a/apps/cli/vitest.config.mts b/apps/cli/vitest.config.mts new file mode 100644 index 000000000..ef0491672 --- /dev/null +++ b/apps/cli/vitest.config.mts @@ -0,0 +1,63 @@ +import path from 'node:path'; +import { defineConfig } from 'vitest/config'; + +const repoRoot = path.resolve(__dirname, '../..'); + +export default defineConfig(() => ({ + root: repoRoot, + cacheDir: 'node_modules/.vite/packages/riviere-cli', + resolve: { + alias: [ + { + find: /^@living-architecture\/riviere-builder-use-cases\/(.*)$/, + replacement: path.resolve(repoRoot, 'packages/riviere-builder/use-cases/src/$1'), + }, + { + find: /^@living-architecture\/riviere-extract-ts-use-cases\/(.*)$/, + replacement: path.resolve(repoRoot, 'packages/riviere-extract-ts/use-cases/src/$1'), + }, + ], + }, + test: { + name: '@living-architecture/riviere-cli', + watch: false, + globals: true, + environment: 'node', + testTimeout: 60_000, + include: [ + 'apps/cli/{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}', + 'packages/riviere-builder/use-cases/src/**/*.{test,spec}.{ts,mts}', + 'packages/riviere-extract-ts/use-cases/src/**/*.{test,spec}.{ts,mts}', + ], + reporters: ['default'], + coverage: { + enabled: true, + reportsDirectory: 'apps/cli/test-output/vitest/coverage', + provider: 'v8' as const, + reporter: ['text', ['lcov', { projectRoot: repoRoot }]] as ['text', ['lcov', { projectRoot: string }]], + include: [ + 'apps/cli/src/**/*.ts', + 'packages/riviere-builder/use-cases/src/**/*.ts', + 'packages/riviere-extract-ts/use-cases/src/**/*.ts', + ], + exclude: [ + '**/*.spec.ts', + '**/__fixtures__/**', + '**/*-input.ts', + '**/*-result.ts', + '**/*test-fixtures.ts', + '**/index.ts', + 'apps/cli/src/features/role-enforcement/entrypoint/role-enforcement/entrypoint.ts', + 'apps/cli/src/shell/bin.ts', + 'apps/cli/src/shell/index.ts', + 'apps/cli/src/shell/role-enforcement-bin.ts', + ], + thresholds: { + lines: 100, + statements: 100, + functions: 100, + branches: 100, + }, + }, + }, +})); diff --git a/apps/docs/CLAUDE.md b/apps/docs/CLAUDE.md index ac52210af..7d149adab 100644 --- a/apps/docs/CLAUDE.md +++ b/apps/docs/CLAUDE.md @@ -42,8 +42,8 @@ Every page serves one of these journeys. New content MUST fit an existing journe | Content | Source | Command | Location | |---------|--------|---------|----------| -| CLI reference | riviere-cli command definitions | `pnpm nx generate-docs riviere-cli` | `packages/riviere-cli/docs/generated/cli-reference.md` → copied to `reference/cli/cli-reference.md` | -| API docs (RiviereBuilder, RiviereQuery) | TypeDoc from source | `pnpm nx typedoc riviere-builder` / `riviere-query` | `reference/api/generated/` | +| CLI reference | riviere-cli command definitions | `pnpm nx generate-docs riviere-cli` | `apps/cli/docs/generated/cli-reference.md` → copied to `reference/cli/cli-reference.md` | +| API docs (RiviereBuilder, RiviereQuery) | TypeDoc from source | `pnpm nx typedoc riviere-builder` | `reference/api/generated/` | | Rivière JSON Schema | Schema package | Built with docs | `public/schema/riviere.schema.json` | **Rules:** diff --git a/apps/docs/extract/ai-assisted/index.md b/apps/docs/extract/ai-assisted/index.md index 87d23ce42..a6234671e 100644 --- a/apps/docs/extract/ai-assisted/index.md +++ b/apps/docs/extract/ai-assisted/index.md @@ -46,7 +46,7 @@ Each step runs in a separate Claude Code (or other) session. This keeps context 1. Open Claude Code (or other) in your project directory 2. Type: ```text - Fetch https://raw.githubusercontent.com/NTCoding/living-architecture/main/packages/riviere-cli/docs/workflow/step-1-understand.md and follow the instructions + Fetch https://raw.githubusercontent.com/NTCoding/living-architecture/main/apps/cli/docs/workflow/step-1-understand.md and follow the instructions ``` 3. Claude analyzes your codebase and creates `.riviere/config/metadata.md` 4. Review the domains Claude identified. Give corrections if needed. @@ -57,7 +57,7 @@ Each step runs in a separate Claude Code (or other) session. This keeps context 1. Open a new Claude Code (or other) session in your project directory 2. Type: ```text - Fetch https://raw.githubusercontent.com/NTCoding/living-architecture/main/packages/riviere-cli/docs/workflow/step-2-define-components.md and follow the instructions + Fetch https://raw.githubusercontent.com/NTCoding/living-architecture/main/apps/cli/docs/workflow/step-2-define-components.md and follow the instructions ``` 3. Claude creates extraction rules in `.riviere/config/component-definitions.md` 4. Review the rules. Give corrections if needed. @@ -68,7 +68,7 @@ Each step runs in a separate Claude Code (or other) session. This keeps context 1. Open a new Claude Code (or other) session in your project directory 2. Type: ```text - Fetch https://raw.githubusercontent.com/NTCoding/living-architecture/main/packages/riviere-cli/docs/workflow/step-3-extract.md and follow the instructions + Fetch https://raw.githubusercontent.com/NTCoding/living-architecture/main/apps/cli/docs/workflow/step-3-extract.md and follow the instructions ``` 3. Claude finds components and adds them to the graph using the CLI 4. Review the component summary @@ -79,7 +79,7 @@ Each step runs in a separate Claude Code (or other) session. This keeps context 1. Open a new Claude Code (or other) session in your project directory 2. Type: ```text - Fetch https://raw.githubusercontent.com/NTCoding/living-architecture/main/packages/riviere-cli/docs/workflow/step-4-link.md and follow the instructions + Fetch https://raw.githubusercontent.com/NTCoding/living-architecture/main/apps/cli/docs/workflow/step-4-link.md and follow the instructions ``` 3. Claude traces flows between components and creates links 4. Review the links @@ -90,7 +90,7 @@ Each step runs in a separate Claude Code (or other) session. This keeps context 1. Open a new Claude Code (or other) session in your project directory 2. Type: ```text - Fetch https://raw.githubusercontent.com/NTCoding/living-architecture/main/packages/riviere-cli/docs/workflow/step-5-enrich.md and follow the instructions + Fetch https://raw.githubusercontent.com/NTCoding/living-architecture/main/apps/cli/docs/workflow/step-5-enrich.md and follow the instructions ``` 3. Claude adds state changes and business rules to DomainOp components 4. Review the enrichments @@ -101,7 +101,7 @@ Each step runs in a separate Claude Code (or other) session. This keeps context 1. Open a new Claude Code (or other) session in your project directory 2. Type: ```text - Fetch https://raw.githubusercontent.com/NTCoding/living-architecture/main/packages/riviere-cli/docs/workflow/step-6-validate.md and follow the instructions + Fetch https://raw.githubusercontent.com/NTCoding/living-architecture/main/apps/cli/docs/workflow/step-6-validate.md and follow the instructions ``` 3. Claude checks for orphans and validates the graph 4. Fix any issues @@ -127,7 +127,7 @@ If Claude misses components or makes mistakes: 1. Give feedback in the current session 2. Or re-run that step with corrections: ```text - Fetch https://raw.githubusercontent.com/NTCoding/living-architecture/main/packages/riviere-cli/docs/workflow/step-3-extract.md and follow the instructions. + Fetch https://raw.githubusercontent.com/NTCoding/living-architecture/main/apps/cli/docs/workflow/step-3-extract.md and follow the instructions. You missed the API controllers in src/api/. Include those. ``` diff --git a/apps/docs/extract/deterministic/index.md b/apps/docs/extract/deterministic/index.md index 4f0d163e6..df0f66c2a 100644 --- a/apps/docs/extract/deterministic/index.md +++ b/apps/docs/extract/deterministic/index.md @@ -36,7 +36,7 @@ Deterministic extraction uses a language-agnostic configuration DSL to define de The config format is **language-agnostic** — defined in JSON Schema, works across TypeScript, Java, Python, etc. -**Current implementation**: TypeScript (`@living-architecture/riviere-extract-ts`) +**Current implementation**: TypeScript (`@living-architecture/riviere-extract-ts-domain-model`) **You can build extractors for other languages** using the same config format. diff --git a/apps/docs/extract/deterministic/typescript/design-for-extraction.md b/apps/docs/extract/deterministic/typescript/design-for-extraction.md index 0b68b7006..7c85377c1 100644 --- a/apps/docs/extract/deterministic/typescript/design-for-extraction.md +++ b/apps/docs/extract/deterministic/typescript/design-for-extraction.md @@ -11,7 +11,7 @@ When code does not follow a stable Convention, extraction can miss Components or In the ecommerce demo app, event publishing uses the `@EventPublisherContainer` decorator: ```typescript -import { EventPublisherContainer } from '@living-architecture/riviere-extract-conventions' +import { EventPublisherContainer } from '@living-architecture/riviere-extract-conventions-published-language' import { eventBus, OrderPlaced } from './events' @EventPublisherContainer @@ -33,7 +33,7 @@ The Golden Path uses shared conventions so extraction can map code to Components ### 1) Event classes use `@Event` ```typescript -import { Event } from '@living-architecture/riviere-extract-conventions' +import { Event } from '@living-architecture/riviere-extract-conventions-published-language' @Event export class OrderPlaced { @@ -44,7 +44,7 @@ export class OrderPlaced { ### 2) Event handlers declare subscribed events ```typescript -import { EventHandlerContainer } from '@living-architecture/riviere-extract-conventions' +import { EventHandlerContainer } from '@living-architecture/riviere-extract-conventions-published-language' @EventHandlerContainer export class PaymentCompletedHandler { diff --git a/apps/docs/extract/deterministic/typescript/enforcement.md b/apps/docs/extract/deterministic/typescript/enforcement.md index ef877fe77..d6c756216 100644 --- a/apps/docs/extract/deterministic/typescript/enforcement.md +++ b/apps/docs/extract/deterministic/typescript/enforcement.md @@ -17,7 +17,7 @@ ESLint catches missing decorators immediately in the IDE and during CI. Install the conventions package (includes ESLint plugin): ```bash -npm install --save-dev @living-architecture/riviere-extract-conventions +npm install --save-dev @living-architecture/riviere-extract-conventions-published-language ``` ## ESLint Configuration @@ -26,7 +26,7 @@ Add the enforcement rule to your ESLint config (flat config format): ```javascript // eslint.config.mjs -import conventionsPlugin from '@living-architecture/riviere-extract-conventions/eslint-plugin' +import conventionsPlugin from '@living-architecture/riviere-extract-conventions-published-language/eslint-plugin' export default [ { @@ -101,7 +101,7 @@ class OrderController { **After (fixed):** ```typescript -import { APIContainer } from '@living-architecture/riviere-extract-conventions' +import { APIContainer } from '@living-architecture/riviere-extract-conventions-published-language' @APIContainer class OrderController { @@ -152,7 +152,7 @@ Valid decorators: - @Custom('type') (custom types) - @Ignore (explicit exclusion) -Import from: @living-architecture/riviere-extract-conventions +Import from: @living-architecture/riviere-extract-conventions-published-language ``` ## Enforcement Concept diff --git a/apps/docs/extract/deterministic/typescript/getting-started.md b/apps/docs/extract/deterministic/typescript/getting-started.md index 638841e26..3f4875e31 100644 --- a/apps/docs/extract/deterministic/typescript/getting-started.md +++ b/apps/docs/extract/deterministic/typescript/getting-started.md @@ -11,7 +11,7 @@ Extract architecture from TypeScript code in 10 minutes using decorators and con **Install the CLI and conventions package:** ```bash -npm install --save-dev @living-architecture/riviere-cli @living-architecture/riviere-extract-conventions +npm install --save-dev @living-architecture/riviere-cli @living-architecture/riviere-extract-conventions-published-language ``` ## Step 1: Annotate Your Code @@ -21,7 +21,7 @@ Add decorators to mark architectural components. **Container decorator** — all public methods become components: ```typescript -import { APIContainer } from '@living-architecture/riviere-extract-conventions' +import { APIContainer } from '@living-architecture/riviere-extract-conventions-published-language' @APIContainer class OrderController { @@ -38,7 +38,7 @@ class OrderController { **Class decorator** — the class itself is the component: ```typescript -import { UseCase } from '@living-architecture/riviere-extract-conventions' +import { UseCase } from '@living-architecture/riviere-extract-conventions-published-language' @UseCase class PlaceOrderUseCase { @@ -57,7 +57,7 @@ The conventions package includes a ready-to-use extraction config that detects a ```bash npx riviere extract \ - --config @living-architecture/riviere-extract-conventions/default-config + --config @living-architecture/riviere-extract-conventions-published-language/default-config ``` **Output (draft components JSON):** @@ -106,7 +106,7 @@ Use `--dry-run` for a quick summary: ```bash npx riviere extract \ - --config @living-architecture/riviere-extract-conventions/default-config \ + --config @living-architecture/riviere-extract-conventions-published-language/default-config \ --dry-run ``` @@ -134,15 +134,15 @@ The simplest way to add multiple modules is with `extends`. Inherit all detectio modules: - name: "orders" path: "src/orders/**/*.ts" - extends: "@living-architecture/riviere-extract-conventions" + extends: "@living-architecture/riviere-extract-conventions-published-language" - name: "shipping" path: "src/shipping/**/*.ts" - extends: "@living-architecture/riviere-extract-conventions" + extends: "@living-architecture/riviere-extract-conventions-published-language" - name: "inventory" path: "src/inventory/**/*.ts" - extends: "@living-architecture/riviere-extract-conventions" + extends: "@living-architecture/riviere-extract-conventions-published-language" ``` Override specific rules when needed: @@ -151,7 +151,7 @@ Override specific rules when needed: modules: - name: "orders" path: "src/orders/**/*.ts" - extends: "@living-architecture/riviere-extract-conventions" + extends: "@living-architecture/riviere-extract-conventions-published-language" event: { notUsed: true } # Override: no events in this module ``` @@ -169,13 +169,13 @@ modules: inClassWith: hasDecorator: name: "APIContainer" - from: "@living-architecture/riviere-extract-conventions" + from: "@living-architecture/riviere-extract-conventions-published-language" useCase: find: "classes" where: hasDecorator: name: "UseCase" - from: "@living-architecture/riviere-extract-conventions" + from: "@living-architecture/riviere-extract-conventions-published-language" domainOp: { notUsed: true } event: { notUsed: true } eventHandler: { notUsed: true } diff --git a/apps/docs/extract/deterministic/typescript/workflow/index.md b/apps/docs/extract/deterministic/typescript/workflow/index.md index 030117811..5f1d35f8f 100644 --- a/apps/docs/extract/deterministic/typescript/workflow/index.md +++ b/apps/docs/extract/deterministic/typescript/workflow/index.md @@ -28,7 +28,7 @@ Standardizing how architecture components are implemented (decorators, JSDoc tag Open a terminal in your project directory and install the CLI and conventions package: ```bash -npm install --save-dev @living-architecture/riviere-cli @living-architecture/riviere-extract-conventions +npm install --save-dev @living-architecture/riviere-cli @living-architecture/riviere-extract-conventions-published-language ``` Then use `npx riviere ...` @@ -62,7 +62,7 @@ This step uses the TypeScript extractor instead of AI. 2. **Annotate your code** (if using decorators): ```typescript - import { UseCase, APIContainer } from '@living-architecture/riviere-extract-conventions' + import { UseCase, APIContainer } from '@living-architecture/riviere-extract-conventions-published-language' @APIContainer class OrderController { @@ -81,11 +81,11 @@ This step uses the TypeScript extractor instead of AI. modules: - name: 'orders' path: 'src/orders/**/*.ts' - extends: '@living-architecture/riviere-extract-conventions' + extends: '@living-architecture/riviere-extract-conventions-published-language' - name: 'shipping' path: 'src/shipping/**/*.ts' - extends: '@living-architecture/riviere-extract-conventions' + extends: '@living-architecture/riviere-extract-conventions-published-language' ``` 4. **Run extraction**: diff --git a/apps/docs/extract/deterministic/typescript/workflow/step-3-extract.md b/apps/docs/extract/deterministic/typescript/workflow/step-3-extract.md index 6c24e3504..5db54788e 100644 --- a/apps/docs/extract/deterministic/typescript/workflow/step-3-extract.md +++ b/apps/docs/extract/deterministic/typescript/workflow/step-3-extract.md @@ -16,7 +16,7 @@ This step uses config-driven detection instead of AI. Components are found by sc **Install the CLI and conventions package:** ```bash -npm install --save-dev @living-architecture/riviere-cli @living-architecture/riviere-extract-conventions +npm install --save-dev @living-architecture/riviere-cli @living-architecture/riviere-extract-conventions-published-language ``` ::: tip AI-Assisted Config Generation @@ -45,13 +45,13 @@ You can mix strategies across different modules. Install the conventions package and annotate your code: ```bash -npm install @living-architecture/riviere-extract-conventions +npm install @living-architecture/riviere-extract-conventions-published-language ``` **Container decorator** — all public methods become components: ```typescript -import { APIContainer } from '@living-architecture/riviere-extract-conventions' +import { APIContainer } from '@living-architecture/riviere-extract-conventions-published-language' @APIContainer class OrderController { @@ -64,7 +64,7 @@ class OrderController { **Class decorator** — the class itself is the component: ```typescript -import { UseCase } from '@living-architecture/riviere-extract-conventions' +import { UseCase } from '@living-architecture/riviere-extract-conventions-published-language' @UseCase class PlaceOrderUseCase { @@ -109,11 +109,11 @@ Inherit detection rules from the conventions package: modules: - name: 'orders' path: 'src/orders/**/*.ts' - extends: '@living-architecture/riviere-extract-conventions' + extends: '@living-architecture/riviere-extract-conventions-published-language' - name: 'shipping' path: 'src/shipping/**/*.ts' - extends: '@living-architecture/riviere-extract-conventions' + extends: '@living-architecture/riviere-extract-conventions-published-language' ``` ### Custom Config @@ -131,14 +131,14 @@ modules: inClassWith: hasDecorator: name: 'APIContainer' - from: '@living-architecture/riviere-extract-conventions' + from: '@living-architecture/riviere-extract-conventions-published-language' useCase: find: 'classes' where: hasDecorator: name: 'UseCase' - from: '@living-architecture/riviere-extract-conventions' + from: '@living-architecture/riviere-extract-conventions-published-language' domainOp: { notUsed: true } event: { notUsed: true } @@ -155,7 +155,7 @@ modules: # Decorators - name: 'orders' path: 'src/orders/**/*.ts' - extends: '@living-architecture/riviere-extract-conventions' + extends: '@living-architecture/riviere-extract-conventions-published-language' # JSDoc - name: 'shipping' @@ -343,7 +343,7 @@ api: inClassWith: hasDecorator: name: 'APIContainer' - from: '@living-architecture/riviere-extract-conventions' + from: '@living-architecture/riviere-extract-conventions-published-language' extract: apiType: { literal: 'REST' } httpMethod: { fromDecoratorName: true } diff --git a/apps/docs/get-started/library-vs-cli.md b/apps/docs/get-started/library-vs-cli.md index a59c8eb20..95599b492 100644 --- a/apps/docs/get-started/library-vs-cli.md +++ b/apps/docs/get-started/library-vs-cli.md @@ -63,7 +63,7 @@ The Library provides **programmatic control** for building graphs in TypeScript 5. Validate and export the graph ```typescript -import { RiviereBuilder } from '@living-architecture/riviere-builder' +import { RiviereBuilder } from '@living-architecture/riviere-builder-domain-model' const builder = new RiviereBuilder({ name: 'my-service', diff --git a/apps/docs/get-started/quick-start.md b/apps/docs/get-started/quick-start.md index 20ee513d6..d6ead247d 100644 --- a/apps/docs/get-started/quick-start.md +++ b/apps/docs/get-started/quick-start.md @@ -11,13 +11,13 @@ Build your first Riviere graph in 5 minutes. ## Installation ```bash -npm install @living-architecture/riviere-builder +npm install @living-architecture/riviere-builder-domain-model ``` ## Basic Usage ```typescript -import { RiviereBuilder } from '@living-architecture/riviere-builder' +import { RiviereBuilder } from '@living-architecture/riviere-builder-domain-model' const builder = new RiviereBuilder({ name: 'my-service', diff --git a/apps/docs/index.md b/apps/docs/index.md index 66f2a459a..75430c026 100644 --- a/apps/docs/index.md +++ b/apps/docs/index.md @@ -119,8 +119,7 @@ Open `http://localhost:5173/eclair/` | Package | Description | |---------|-------------| | `@living-architecture/riviere-cli` | CLI for extraction and graph building | -| `@living-architecture/riviere-builder` | Node.js library for building graphs | -| `@living-architecture/riviere-query` | Browser-safe library for querying graphs | -| `@living-architecture/riviere-extract-config` | Extraction config schema and validation | -| `@living-architecture/riviere-extract-conventions` | TypeScript decorators for component marking | -| `@living-architecture/riviere-extract-ts` | TypeScript component extractor | +| `@living-architecture/riviere-builder-domain-model` | Browser-safe library for building and querying graphs | +| `@living-architecture/riviere-extract-config-published-language` | Extraction config schema and validation | +| `@living-architecture/riviere-extract-conventions-published-language` | TypeScript decorators for component marking | +| `@living-architecture/riviere-extract-ts-domain-model` | TypeScript component extractor | diff --git a/apps/docs/package.json b/apps/docs/package.json index a1a80448b..abffae59f 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -23,10 +23,10 @@ ], "options": { "commands": [ - "cp ../../packages/riviere-cli/docs/workflow/step-*.md extract/ai-assisted/", - "cp ../../packages/riviere-cli/docs/generated/cli-reference.md reference/cli/cli-reference.md", - "cp ../../packages/riviere-extract-config/docs/generated/predicates.md reference/extraction-config/predicates.md", - "cp ../../packages/riviere-extract-config/docs/generated/schema.md reference/extraction-config/schema.md", + "cp ../../apps/cli/docs/workflow/step-*.md extract/ai-assisted/", + "cp ../../apps/cli/docs/generated/cli-reference.md reference/cli/cli-reference.md", + "cp ../../packages/riviere-extract-config/published-language/docs/generated/predicates.md reference/extraction-config/predicates.md", + "cp ../../packages/riviere-extract-config/published-language/docs/generated/schema.md reference/extraction-config/schema.md", "pnpm exec vitepress build" ], "cwd": "apps/docs", @@ -38,10 +38,10 @@ "continuous": true, "options": { "commands": [ - "cp ../../packages/riviere-cli/docs/workflow/step-*.md extract/ai-assisted/", - "cp ../../packages/riviere-cli/docs/generated/cli-reference.md reference/cli/cli-reference.md", - "cp ../../packages/riviere-extract-config/docs/generated/predicates.md reference/extraction-config/predicates.md", - "cp ../../packages/riviere-extract-config/docs/generated/schema.md reference/extraction-config/schema.md", + "cp ../../apps/cli/docs/workflow/step-*.md extract/ai-assisted/", + "cp ../../apps/cli/docs/generated/cli-reference.md reference/cli/cli-reference.md", + "cp ../../packages/riviere-extract-config/published-language/docs/generated/predicates.md reference/extraction-config/predicates.md", + "cp ../../packages/riviere-extract-config/published-language/docs/generated/schema.md reference/extraction-config/schema.md", "pnpm exec vitepress dev" ], "cwd": "apps/docs", diff --git a/apps/docs/project.json b/apps/docs/project.json index abeca09c5..36743033e 100644 --- a/apps/docs/project.json +++ b/apps/docs/project.json @@ -8,7 +8,7 @@ } }, "build": { - "dependsOn": ["lint", "^build", "riviere-query:typedoc", "riviere-builder:typedoc", "riviere-cli:generate-docs"] + "dependsOn": ["lint", "^build", "riviere-builder:typedoc", "riviere-cli:generate-docs"] } } } diff --git a/apps/docs/reference/api/generated/riviere-builder/README.md b/apps/docs/reference/api/generated/riviere-builder/README.md index d73260112..15fc5d99c 100644 --- a/apps/docs/reference/api/generated/riviere-builder/README.md +++ b/apps/docs/reference/api/generated/riviere-builder/README.md @@ -2,7 +2,7 @@ pageClass: reference --- -# @living-architecture/riviere-builder +# @living-architecture/riviere-builder-domain-model ## Classes @@ -24,33 +24,9 @@ pageClass: reference - [RelationshipTypeAlreadyDefinedError](classes/RelationshipTypeAlreadyDefinedError.md) - [RelationshipTypeNotFoundError](classes/RelationshipTypeNotFoundError.md) - [RiviereBuilder](classes/RiviereBuilder.md) +- [RiviereQuery](classes/RiviereQuery.md) - [SourceConflictError](classes/SourceConflictError.md) -## Interfaces - -- [APIInput](interfaces/APIInput.md) -- [BuilderOptions](interfaces/BuilderOptions.md) -- [BuilderStats](interfaces/BuilderStats.md) -- [BuilderWarning](interfaces/BuilderWarning.md) -- [ComponentIdParts](interfaces/ComponentIdParts.md) -- [CustomInput](interfaces/CustomInput.md) -- [CustomTypeInput](interfaces/CustomTypeInput.md) -- [DomainInput](interfaces/DomainInput.md) -- [DomainOpInput](interfaces/DomainOpInput.md) -- [EnrichmentInput](interfaces/EnrichmentInput.md) -- [EventHandlerInput](interfaces/EventHandlerInput.md) -- [EventInput](interfaces/EventInput.md) -- [ExternalLinkInput](interfaces/ExternalLinkInput.md) -- [LinkInput](interfaces/LinkInput.md) -- [NearMatchMismatch](interfaces/NearMatchMismatch.md) -- [NearMatchOptions](interfaces/NearMatchOptions.md) -- [NearMatchQuery](interfaces/NearMatchQuery.md) -- [NearMatchResult](interfaces/NearMatchResult.md) -- [RelationshipTypeInput](interfaces/RelationshipTypeInput.md) -- [UIInput](interfaces/UIInput.md) -- [UpsertOptions](interfaces/UpsertOptions.md) -- [UseCaseInput](interfaces/UseCaseInput.md) - ## Functions - [findNearMatches](functions/findNearMatches.md) diff --git a/apps/docs/reference/api/generated/riviere-builder/classes/BuildValidationError.md b/apps/docs/reference/api/generated/riviere-builder/classes/BuildValidationError.md index 62d6d2a6c..c878638b6 100644 --- a/apps/docs/reference/api/generated/riviere-builder/classes/BuildValidationError.md +++ b/apps/docs/reference/api/generated/riviere-builder/classes/BuildValidationError.md @@ -4,7 +4,7 @@ pageClass: reference # Class: BuildValidationError -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:185](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L185) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:185 ## Riviere-role @@ -20,7 +20,7 @@ domain-error > **new BuildValidationError**(`messages`): `BuildValidationError` -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:188](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L188) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:188 #### Parameters @@ -90,7 +90,7 @@ Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/li > `readonly` **validationMessages**: `string`[] -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:186](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L186) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:186 *** diff --git a/apps/docs/reference/api/generated/riviere-builder/classes/ComponentId.md b/apps/docs/reference/api/generated/riviere-builder/classes/ComponentId.md index c55b4760b..3b36c9c90 100644 --- a/apps/docs/reference/api/generated/riviere-builder/classes/ComponentId.md +++ b/apps/docs/reference/api/generated/riviere-builder/classes/ComponentId.md @@ -4,23 +4,11 @@ pageClass: reference # Class: ComponentId -Defined in: packages/riviere-schema/dist/component-id.d.ts:33 +Defined in: packages/riviere-schema/published-language/dist/published-language/component-id.d.ts:10 -Represents a structured component identifier. +## Riviere-role -Component IDs follow the format `{domain}:{module}:{type}:{name}` in kebab-case. - -## Example - -```typescript -const id = ComponentId.create({ - domain: 'orders', - module: 'checkout', - type: 'api', - name: 'Create Order' -}) -id.toString() // 'orders:checkout:api:create-order' -``` +value-object ## Methods @@ -28,98 +16,70 @@ id.toString() // 'orders:checkout:api:create-order' > **name**(): `string` -Defined in: packages/riviere-schema/dist/component-id.d.ts:79 - -Returns the name segment of the component ID. +Defined in: packages/riviere-schema/published-language/dist/published-language/component-id.d.ts:23 #### Returns `string` -The kebab-case name portion - *** ### toString() > **toString**(): `string` -Defined in: packages/riviere-schema/dist/component-id.d.ts:73 - -Returns the full component ID string. +Defined in: packages/riviere-schema/published-language/dist/published-language/component-id.d.ts:22 #### Returns `string` -Full ID in format `domain:module:type:name` - *** -### create() - -> `static` **create**(`parts`): `ComponentId` +### parse() -Defined in: packages/riviere-schema/dist/component-id.d.ts:53 +> `static` **parse**(`value`): `ComponentIdParseResult` -Creates a ComponentId from individual parts. +Defined in: packages/riviere-schema/published-language/dist/published-language/component-id.d.ts:15 #### Parameters -##### parts - -[`ComponentIdParts`](../interfaces/ComponentIdParts.md) +##### value -Domain, module, type, and name segments +`string` #### Returns -`ComponentId` - -A new ComponentId instance - -#### Example - -```typescript -const id = ComponentId.create({ - domain: 'orders', - module: 'checkout', - type: 'api', - name: 'Create Order' -}) -``` +`ComponentIdParseResult` *** -### parse() - -> `static` **parse**(`id`): `ComponentId` +### parseFromParts() -Defined in: packages/riviere-schema/dist/component-id.d.ts:67 +> `static` **parseFromParts**(`parts`): `ComponentId` -Parses a string ID into a ComponentId instance. +Defined in: packages/riviere-schema/published-language/dist/published-language/component-id.d.ts:16 #### Parameters -##### id +##### parts + +###### domain `string` -String in format `domain:module:type:name` +###### module -#### Returns +`string` -`ComponentId` +###### name -A ComponentId instance +`string` -#### Throws +###### type -If the format is invalid +`string` -#### Example +#### Returns -```typescript -const id = ComponentId.parse('orders:checkout:api:create-order') -id.name() // 'create-order' -``` +`ComponentId` diff --git a/apps/docs/reference/api/generated/riviere-builder/classes/ComponentNotFoundError.md b/apps/docs/reference/api/generated/riviere-builder/classes/ComponentNotFoundError.md index 40b8ec6aa..82a502000 100644 --- a/apps/docs/reference/api/generated/riviere-builder/classes/ComponentNotFoundError.md +++ b/apps/docs/reference/api/generated/riviere-builder/classes/ComponentNotFoundError.md @@ -4,7 +4,7 @@ pageClass: reference # Class: ComponentNotFoundError -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:80](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L80) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:80 ## Riviere-role @@ -20,7 +20,7 @@ domain-error > **new ComponentNotFoundError**(`componentId`, `suggestions`): `ComponentNotFoundError` -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:84](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L84) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:84 #### Parameters @@ -58,7 +58,7 @@ Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/li > `readonly` **componentId**: `string` -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:81](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L81) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:81 *** @@ -102,7 +102,7 @@ Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/li > `readonly` **suggestions**: `string`[] -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:82](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L82) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:82 *** diff --git a/apps/docs/reference/api/generated/riviere-builder/classes/ComponentTypeMismatchError.md b/apps/docs/reference/api/generated/riviere-builder/classes/ComponentTypeMismatchError.md index a5ba51d61..e2c48c92a 100644 --- a/apps/docs/reference/api/generated/riviere-builder/classes/ComponentTypeMismatchError.md +++ b/apps/docs/reference/api/generated/riviere-builder/classes/ComponentTypeMismatchError.md @@ -4,7 +4,7 @@ pageClass: reference # Class: ComponentTypeMismatchError -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:63](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L63) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:63 ## Riviere-role @@ -20,7 +20,7 @@ domain-error > **new ComponentTypeMismatchError**(`componentId`, `existingType`, `incomingType`): `ComponentTypeMismatchError` -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:68](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L68) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:68 #### Parameters @@ -62,7 +62,7 @@ Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/li > `readonly` **componentId**: `string` -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:64](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L64) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:64 *** @@ -70,7 +70,7 @@ Defined in: [packages/riviere-builder/src/features/building/domain/construction/ > `readonly` **existingType**: `string` -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:65](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L65) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:65 *** @@ -78,7 +78,7 @@ Defined in: [packages/riviere-builder/src/features/building/domain/construction/ > `readonly` **incomingType**: `string` -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:66](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L66) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:66 *** diff --git a/apps/docs/reference/api/generated/riviere-builder/classes/CustomTypeAlreadyDefinedError.md b/apps/docs/reference/api/generated/riviere-builder/classes/CustomTypeAlreadyDefinedError.md index 35a592217..b9066a83c 100644 --- a/apps/docs/reference/api/generated/riviere-builder/classes/CustomTypeAlreadyDefinedError.md +++ b/apps/docs/reference/api/generated/riviere-builder/classes/CustomTypeAlreadyDefinedError.md @@ -4,7 +4,7 @@ pageClass: reference # Class: CustomTypeAlreadyDefinedError -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:98](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L98) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:98 ## Riviere-role @@ -20,7 +20,7 @@ domain-error > **new CustomTypeAlreadyDefinedError**(`typeName`): `CustomTypeAlreadyDefinedError` -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:101](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L101) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:101 #### Parameters @@ -90,7 +90,7 @@ Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/li > `readonly` **typeName**: `string` -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:99](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L99) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:99 *** diff --git a/apps/docs/reference/api/generated/riviere-builder/classes/CustomTypeNotFoundError.md b/apps/docs/reference/api/generated/riviere-builder/classes/CustomTypeNotFoundError.md index 34f73893d..8e22ffe9f 100644 --- a/apps/docs/reference/api/generated/riviere-builder/classes/CustomTypeNotFoundError.md +++ b/apps/docs/reference/api/generated/riviere-builder/classes/CustomTypeNotFoundError.md @@ -4,7 +4,7 @@ pageClass: reference # Class: CustomTypeNotFoundError -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:35](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L35) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:35 ## Riviere-role @@ -20,7 +20,7 @@ domain-error > **new CustomTypeNotFoundError**(`customTypeName`, `definedTypes`): `CustomTypeNotFoundError` -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:39](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L39) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:39 #### Parameters @@ -58,7 +58,7 @@ Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/li > `readonly` **customTypeName**: `string` -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:36](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L36) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:36 *** @@ -66,7 +66,7 @@ Defined in: [packages/riviere-builder/src/features/building/domain/construction/ > `readonly` **definedTypes**: `string`[] -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:37](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L37) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:37 *** diff --git a/apps/docs/reference/api/generated/riviere-builder/classes/DomainNotFoundError.md b/apps/docs/reference/api/generated/riviere-builder/classes/DomainNotFoundError.md index 26347ca32..c1f19cda4 100644 --- a/apps/docs/reference/api/generated/riviere-builder/classes/DomainNotFoundError.md +++ b/apps/docs/reference/api/generated/riviere-builder/classes/DomainNotFoundError.md @@ -4,7 +4,7 @@ pageClass: reference # Class: DomainNotFoundError -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:24](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L24) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:24 ## Riviere-role @@ -20,7 +20,7 @@ domain-error > **new DomainNotFoundError**(`domainName`): `DomainNotFoundError` -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:27](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L27) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:27 #### Parameters @@ -54,7 +54,7 @@ Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/li > `readonly` **domainName**: `string` -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:25](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L25) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:25 *** diff --git a/apps/docs/reference/api/generated/riviere-builder/classes/DuplicateComponentError.md b/apps/docs/reference/api/generated/riviere-builder/classes/DuplicateComponentError.md index e857cccce..1c96cf128 100644 --- a/apps/docs/reference/api/generated/riviere-builder/classes/DuplicateComponentError.md +++ b/apps/docs/reference/api/generated/riviere-builder/classes/DuplicateComponentError.md @@ -4,7 +4,7 @@ pageClass: reference # Class: DuplicateComponentError -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:52](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L52) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:52 ## Riviere-role @@ -20,7 +20,7 @@ domain-error > **new DuplicateComponentError**(`componentId`): `DuplicateComponentError` -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:55](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L55) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:55 #### Parameters @@ -54,7 +54,7 @@ Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/li > `readonly` **componentId**: `string` -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:53](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L53) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:53 *** diff --git a/apps/docs/reference/api/generated/riviere-builder/classes/DuplicateDomainError.md b/apps/docs/reference/api/generated/riviere-builder/classes/DuplicateDomainError.md index 71e603163..ab1de185e 100644 --- a/apps/docs/reference/api/generated/riviere-builder/classes/DuplicateDomainError.md +++ b/apps/docs/reference/api/generated/riviere-builder/classes/DuplicateDomainError.md @@ -4,7 +4,7 @@ pageClass: reference # Class: DuplicateDomainError -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:2](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L2) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:2 ## Riviere-role @@ -20,7 +20,7 @@ domain-error > **new DuplicateDomainError**(`domainName`): `DuplicateDomainError` -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:5](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L5) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:5 #### Parameters @@ -54,7 +54,7 @@ Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/li > `readonly` **domainName**: `string` -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:3](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L3) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:3 *** diff --git a/apps/docs/reference/api/generated/riviere-builder/classes/DuplicateLinkError.md b/apps/docs/reference/api/generated/riviere-builder/classes/DuplicateLinkError.md index 53cc0f675..205aec8fe 100644 --- a/apps/docs/reference/api/generated/riviere-builder/classes/DuplicateLinkError.md +++ b/apps/docs/reference/api/generated/riviere-builder/classes/DuplicateLinkError.md @@ -4,7 +4,7 @@ pageClass: reference # Class: DuplicateLinkError -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:137](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L137) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:137 ## Riviere-role @@ -20,7 +20,7 @@ domain-error > **new DuplicateLinkError**(`linkId`): `DuplicateLinkError` -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:140](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L140) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:140 #### Parameters @@ -54,7 +54,7 @@ Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/li > `readonly` **linkId**: `string` -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:138](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L138) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:138 *** diff --git a/apps/docs/reference/api/generated/riviere-builder/classes/InvalidEnrichmentTargetError.md b/apps/docs/reference/api/generated/riviere-builder/classes/InvalidEnrichmentTargetError.md index 4fbe57719..250442bd3 100644 --- a/apps/docs/reference/api/generated/riviere-builder/classes/InvalidEnrichmentTargetError.md +++ b/apps/docs/reference/api/generated/riviere-builder/classes/InvalidEnrichmentTargetError.md @@ -4,7 +4,7 @@ pageClass: reference # Class: InvalidEnrichmentTargetError -Defined in: [packages/riviere-builder/src/features/building/domain/enrichment/enrichment-errors.ts:2](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/enrichment/enrichment-errors.ts#L2) +Defined in: packages/riviere-builder/domain-model/src/domain/enrichment/enrichment-errors.ts:2 ## Riviere-role @@ -20,7 +20,7 @@ domain-error > **new InvalidEnrichmentTargetError**(`componentId`, `componentType`): `InvalidEnrichmentTargetError` -Defined in: [packages/riviere-builder/src/features/building/domain/enrichment/enrichment-errors.ts:6](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/enrichment/enrichment-errors.ts#L6) +Defined in: packages/riviere-builder/domain-model/src/domain/enrichment/enrichment-errors.ts:6 #### Parameters @@ -58,7 +58,7 @@ Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/li > `readonly` **componentId**: `string` -Defined in: [packages/riviere-builder/src/features/building/domain/enrichment/enrichment-errors.ts:3](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/enrichment/enrichment-errors.ts#L3) +Defined in: packages/riviere-builder/domain-model/src/domain/enrichment/enrichment-errors.ts:3 *** @@ -66,7 +66,7 @@ Defined in: [packages/riviere-builder/src/features/building/domain/enrichment/en > `readonly` **componentType**: `string` -Defined in: [packages/riviere-builder/src/features/building/domain/enrichment/enrichment-errors.ts:4](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/enrichment/enrichment-errors.ts#L4) +Defined in: packages/riviere-builder/domain-model/src/domain/enrichment/enrichment-errors.ts:4 *** diff --git a/apps/docs/reference/api/generated/riviere-builder/classes/InvalidGraphError.md b/apps/docs/reference/api/generated/riviere-builder/classes/InvalidGraphError.md index 117cf06ef..3ddf13c2e 100644 --- a/apps/docs/reference/api/generated/riviere-builder/classes/InvalidGraphError.md +++ b/apps/docs/reference/api/generated/riviere-builder/classes/InvalidGraphError.md @@ -4,7 +4,7 @@ pageClass: reference # Class: InvalidGraphError -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:161](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L161) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:161 ## Riviere-role @@ -20,7 +20,7 @@ domain-error > **new InvalidGraphError**(`reason`): `InvalidGraphError` -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:162](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L162) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:162 #### Parameters diff --git a/apps/docs/reference/api/generated/riviere-builder/classes/MissingDomainsError.md b/apps/docs/reference/api/generated/riviere-builder/classes/MissingDomainsError.md index 1ba212618..2b3ff1fde 100644 --- a/apps/docs/reference/api/generated/riviere-builder/classes/MissingDomainsError.md +++ b/apps/docs/reference/api/generated/riviere-builder/classes/MissingDomainsError.md @@ -4,7 +4,7 @@ pageClass: reference # Class: MissingDomainsError -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:177](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L177) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:177 ## Riviere-role @@ -20,7 +20,7 @@ domain-error > **new MissingDomainsError**(): `MissingDomainsError` -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:178](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L178) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:178 #### Returns diff --git a/apps/docs/reference/api/generated/riviere-builder/classes/MissingRequiredPropertiesError.md b/apps/docs/reference/api/generated/riviere-builder/classes/MissingRequiredPropertiesError.md index 798f3f955..415b2cadb 100644 --- a/apps/docs/reference/api/generated/riviere-builder/classes/MissingRequiredPropertiesError.md +++ b/apps/docs/reference/api/generated/riviere-builder/classes/MissingRequiredPropertiesError.md @@ -4,7 +4,7 @@ pageClass: reference # Class: MissingRequiredPropertiesError -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:148](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L148) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:148 ## Riviere-role @@ -20,7 +20,7 @@ domain-error > **new MissingRequiredPropertiesError**(`customTypeName`, `missingKeys`): `MissingRequiredPropertiesError` -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:152](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L152) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:152 #### Parameters @@ -58,7 +58,7 @@ Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/li > `readonly` **customTypeName**: `string` -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:149](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L149) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:149 *** @@ -78,7 +78,7 @@ Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/li > `readonly` **missingKeys**: `string`[] -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:150](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L150) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:150 *** diff --git a/apps/docs/reference/api/generated/riviere-builder/classes/MissingSourcesError.md b/apps/docs/reference/api/generated/riviere-builder/classes/MissingSourcesError.md index 7192d0610..6b834264d 100644 --- a/apps/docs/reference/api/generated/riviere-builder/classes/MissingSourcesError.md +++ b/apps/docs/reference/api/generated/riviere-builder/classes/MissingSourcesError.md @@ -4,7 +4,7 @@ pageClass: reference # Class: MissingSourcesError -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:169](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L169) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:169 ## Riviere-role @@ -20,7 +20,7 @@ domain-error > **new MissingSourcesError**(): `MissingSourcesError` -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:170](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L170) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:170 #### Returns diff --git a/apps/docs/reference/api/generated/riviere-builder/classes/RelationshipTypeAlreadyDefinedError.md b/apps/docs/reference/api/generated/riviere-builder/classes/RelationshipTypeAlreadyDefinedError.md index 2f6f67ad5..a57c733fb 100644 --- a/apps/docs/reference/api/generated/riviere-builder/classes/RelationshipTypeAlreadyDefinedError.md +++ b/apps/docs/reference/api/generated/riviere-builder/classes/RelationshipTypeAlreadyDefinedError.md @@ -4,7 +4,7 @@ pageClass: reference # Class: RelationshipTypeAlreadyDefinedError -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:109](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L109) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:109 ## Riviere-role @@ -20,7 +20,7 @@ domain-error > **new RelationshipTypeAlreadyDefinedError**(`typeName`): `RelationshipTypeAlreadyDefinedError` -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:112](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L112) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:112 #### Parameters @@ -90,7 +90,7 @@ Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/li > `readonly` **typeName**: `string` -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:110](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L110) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:110 *** diff --git a/apps/docs/reference/api/generated/riviere-builder/classes/RelationshipTypeNotFoundError.md b/apps/docs/reference/api/generated/riviere-builder/classes/RelationshipTypeNotFoundError.md index fbb24adf7..b3f28301d 100644 --- a/apps/docs/reference/api/generated/riviere-builder/classes/RelationshipTypeNotFoundError.md +++ b/apps/docs/reference/api/generated/riviere-builder/classes/RelationshipTypeNotFoundError.md @@ -4,7 +4,7 @@ pageClass: reference # Class: RelationshipTypeNotFoundError -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:120](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L120) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:120 ## Riviere-role @@ -20,7 +20,7 @@ domain-error > **new RelationshipTypeNotFoundError**(`relationshipType`, `definedTypes`): `RelationshipTypeNotFoundError` -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:124](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L124) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:124 #### Parameters @@ -58,7 +58,7 @@ Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/li > `readonly` **definedTypes**: `string`[] -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:122](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L122) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:122 *** @@ -90,7 +90,7 @@ Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/li > `readonly` **relationshipType**: `string` -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:121](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L121) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:121 *** diff --git a/apps/docs/reference/api/generated/riviere-builder/classes/RiviereBuilder.md b/apps/docs/reference/api/generated/riviere-builder/classes/RiviereBuilder.md index 409812c64..4e98490f7 100644 --- a/apps/docs/reference/api/generated/riviere-builder/classes/RiviereBuilder.md +++ b/apps/docs/reference/api/generated/riviere-builder/classes/RiviereBuilder.md @@ -4,7 +4,7 @@ pageClass: reference # Class: RiviereBuilder -Defined in: [packages/riviere-builder/src/features/building/domain/builder-facade.ts:76](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/builder-facade.ts#L76) +Defined in: packages/riviere-builder/domain-model/src/domain/builder-facade.ts:143 Programmatically construct Riviere architecture graphs. @@ -21,7 +21,7 @@ aggregate > `readonly` **graphPath**: `string` -Defined in: [packages/riviere-builder/src/features/building/domain/builder-facade.ts:79](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/builder-facade.ts#L79) +Defined in: packages/riviere-builder/domain-model/src/domain/builder-facade.ts:146 ## Methods @@ -29,7 +29,7 @@ Defined in: [packages/riviere-builder/src/features/building/domain/builder-facad > **addApi**(`input`): `APIComponent` -Defined in: [packages/riviere-builder/src/features/building/domain/builder-facade.ts:152](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/builder-facade.ts#L152) +Defined in: packages/riviere-builder/domain-model/src/domain/builder-facade.ts:226 Adds an API component to the graph. @@ -37,7 +37,7 @@ Adds an API component to the graph. ##### input -[`APIInput`](../interfaces/APIInput.md) +`APIInput` API component properties @@ -53,7 +53,7 @@ The created API component > **addCustom**(`input`): `CustomComponent` -Defined in: [packages/riviere-builder/src/features/building/domain/builder-facade.ts:270](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/builder-facade.ts#L270) +Defined in: packages/riviere-builder/domain-model/src/domain/builder-facade.ts:379 Adds a Custom component to the graph. @@ -61,7 +61,7 @@ Adds a Custom component to the graph. ##### input -[`CustomInput`](../interfaces/CustomInput.md) +`CustomInput` Custom component properties @@ -77,7 +77,7 @@ The created Custom component > **addDomain**(`input`): `void` -Defined in: [packages/riviere-builder/src/features/building/domain/builder-facade.ts:122](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/builder-facade.ts#L122) +Defined in: packages/riviere-builder/domain-model/src/domain/builder-facade.ts:189 Adds a new domain to the graph. @@ -85,7 +85,7 @@ Adds a new domain to the graph. ##### input -[`DomainInput`](../interfaces/DomainInput.md) +`DomainInput` Domain name and description @@ -99,7 +99,7 @@ Domain name and description > **addDomainOp**(`input`): `DomainOpComponent` -Defined in: [packages/riviere-builder/src/features/building/domain/builder-facade.ts:192](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/builder-facade.ts#L192) +Defined in: packages/riviere-builder/domain-model/src/domain/builder-facade.ts:280 Adds a DomainOp component to the graph. @@ -107,7 +107,7 @@ Adds a DomainOp component to the graph. ##### input -[`DomainOpInput`](../interfaces/DomainOpInput.md) +`DomainOpInput` DomainOp component properties @@ -123,7 +123,7 @@ The created DomainOp component > **addEvent**(`input`): `EventComponent` -Defined in: [packages/riviere-builder/src/features/building/domain/builder-facade.ts:212](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/builder-facade.ts#L212) +Defined in: packages/riviere-builder/domain-model/src/domain/builder-facade.ts:307 Adds an Event component to the graph. @@ -131,7 +131,7 @@ Adds an Event component to the graph. ##### input -[`EventInput`](../interfaces/EventInput.md) +`EventInput` Event component properties @@ -147,7 +147,7 @@ The created Event component > **addEventHandler**(`input`): `EventHandlerComponent` -Defined in: [packages/riviere-builder/src/features/building/domain/builder-facade.ts:232](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/builder-facade.ts#L232) +Defined in: packages/riviere-builder/domain-model/src/domain/builder-facade.ts:334 Adds an EventHandler component to the graph. @@ -155,7 +155,7 @@ Adds an EventHandler component to the graph. ##### input -[`EventHandlerInput`](../interfaces/EventHandlerInput.md) +`EventHandlerInput` EventHandler component properties @@ -171,7 +171,7 @@ The created EventHandler component > **addSource**(`source`): `void` -Defined in: [packages/riviere-builder/src/features/building/domain/builder-facade.ts:113](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/builder-facade.ts#L113) +Defined in: packages/riviere-builder/domain-model/src/domain/builder-facade.ts:180 Adds an additional source repository to the graph. @@ -193,7 +193,7 @@ Source repository information > **addUI**(`input`): `UIComponent` -Defined in: [packages/riviere-builder/src/features/building/domain/builder-facade.ts:132](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/builder-facade.ts#L132) +Defined in: packages/riviere-builder/domain-model/src/domain/builder-facade.ts:199 Adds a UI component to the graph. @@ -201,7 +201,7 @@ Adds a UI component to the graph. ##### input -[`UIInput`](../interfaces/UIInput.md) +`UIInput` UI component properties @@ -217,7 +217,7 @@ The created UI component > **addUseCase**(`input`): `UseCaseComponent` -Defined in: [packages/riviere-builder/src/features/building/domain/builder-facade.ts:172](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/builder-facade.ts#L172) +Defined in: packages/riviere-builder/domain-model/src/domain/builder-facade.ts:253 Adds a UseCase component to the graph. @@ -225,7 +225,7 @@ Adds a UseCase component to the graph. ##### input -[`UseCaseInput`](../interfaces/UseCaseInput.md) +`UseCaseInput` UseCase component properties @@ -241,7 +241,7 @@ The created UseCase component > **build**(): `RiviereGraph` -Defined in: [packages/riviere-builder/src/features/building/domain/builder-facade.ts:384](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/builder-facade.ts#L384) +Defined in: packages/riviere-builder/domain-model/src/domain/builder-facade.ts:510 Validates and returns the completed graph. @@ -257,7 +257,7 @@ Valid RiviereGraph object > **defineCustomType**(`input`): `void` -Defined in: [packages/riviere-builder/src/features/building/domain/builder-facade.ts:251](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/builder-facade.ts#L251) +Defined in: packages/riviere-builder/domain-model/src/domain/builder-facade.ts:360 Defines a custom component type for the graph. @@ -265,7 +265,7 @@ Defines a custom component type for the graph. ##### input -[`CustomTypeInput`](../interfaces/CustomTypeInput.md) +`CustomTypeInput` Custom type definition @@ -279,7 +279,7 @@ Custom type definition > **defineRelationshipType**(`input`): `void` -Defined in: [packages/riviere-builder/src/features/building/domain/builder-facade.ts:260](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/builder-facade.ts#L260) +Defined in: packages/riviere-builder/domain-model/src/domain/builder-facade.ts:369 Defines a relationship type for the graph. @@ -287,7 +287,7 @@ Defines a relationship type for the graph. ##### input -[`RelationshipTypeInput`](../interfaces/RelationshipTypeInput.md) +`RelationshipTypeInput` Relationship type name and description @@ -301,7 +301,7 @@ Relationship type name and description > **enrichComponent**(`id`, `enrichment`): `void` -Defined in: [packages/riviere-builder/src/features/building/domain/builder-facade.ts:290](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/builder-facade.ts#L290) +Defined in: packages/riviere-builder/domain-model/src/domain/builder-facade.ts:406 Enriches a DomainOp component with additional domain details. @@ -315,7 +315,7 @@ The component ID to enrich ##### enrichment -[`EnrichmentInput`](../interfaces/EnrichmentInput.md) +`EnrichmentInput` State changes and business rules to add @@ -329,7 +329,7 @@ State changes and business rules to add > **link**(`input`): `Link` -Defined in: [packages/riviere-builder/src/features/building/domain/builder-facade.ts:311](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/builder-facade.ts#L311) +Defined in: packages/riviere-builder/domain-model/src/domain/builder-facade.ts:437 Creates a link between two components in the graph. @@ -337,7 +337,7 @@ Creates a link between two components in the graph. ##### input -[`LinkInput`](../interfaces/LinkInput.md) +`LinkInput` Link properties including source, target, and type @@ -353,7 +353,7 @@ The created link > **linkExternal**(`input`): `ExternalLink` -Defined in: [packages/riviere-builder/src/features/building/domain/builder-facade.ts:321](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/builder-facade.ts#L321) +Defined in: packages/riviere-builder/domain-model/src/domain/builder-facade.ts:447 Creates a link from a component to an external system. @@ -361,7 +361,7 @@ Creates a link from a component to an external system. ##### input -[`ExternalLinkInput`](../interfaces/ExternalLinkInput.md) +`ExternalLinkInput` External link properties including target system info @@ -375,9 +375,9 @@ The created external link ### nearMatches() -> **nearMatches**(`query`, `options?`): [`NearMatchResult`](../interfaces/NearMatchResult.md)[] +> **nearMatches**(`query`, `options?`): `Readonly`\<\{ `component`: `Component`; `mismatch?`: `Readonly`\<\{ `actual`: `string`; `expected`: `string`; `field`: `"type"` \| `"domain"`; \}\>; `score`: `number`; \}\>[] -Defined in: [packages/riviere-builder/src/features/building/domain/builder-facade.ts:301](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/builder-facade.ts#L301) +Defined in: packages/riviere-builder/domain-model/src/domain/builder-facade.ts:417 Finds components similar to a query for error recovery. @@ -385,19 +385,19 @@ Finds components similar to a query for error recovery. ##### query -[`NearMatchQuery`](../interfaces/NearMatchQuery.md) +`Readonly`\<\{ `domain?`: `string`; `name`: `string`; `type?`: `ComponentType`; \}\> Search criteria including partial ID, name, type, or domain ##### options? -[`NearMatchOptions`](../interfaces/NearMatchOptions.md) +`Readonly`\<\{ `limit?`: `number`; `threshold?`: `number`; \}\> Optional matching thresholds and limits #### Returns -[`NearMatchResult`](../interfaces/NearMatchResult.md)[] +`Readonly`\<\{ `component`: `Component`; `mismatch?`: `Readonly`\<\{ `actual`: `string`; `expected`: `string`; `field`: `"type"` \| `"domain"`; \}\>; `score`: `number`; \}\>[] Array of similar components with similarity scores @@ -407,7 +407,7 @@ Array of similar components with similarity scores > **orphans**(): `string`[] -Defined in: [packages/riviere-builder/src/features/building/domain/builder-facade.ts:357](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/builder-facade.ts#L357) +Defined in: packages/riviere-builder/domain-model/src/domain/builder-facade.ts:483 Returns IDs of components with no incoming or outgoing links. @@ -421,17 +421,17 @@ Array of orphaned component IDs ### query() -> **query**(): `RiviereQuery` +> **query**(): [`RiviereQuery`](RiviereQuery.md) -Defined in: [packages/riviere-builder/src/features/building/domain/builder-facade.ts:366](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/builder-facade.ts#L366) +Defined in: packages/riviere-builder/domain-model/src/domain/builder-facade.ts:492 -Returns a RiviereQuery instance for the current graph state. +Returns query capabilities for the current graph state. #### Returns -`RiviereQuery` +[`RiviereQuery`](RiviereQuery.md) -RiviereQuery instance for the current graph +A snapshot that can be queried without mutating the builder *** @@ -439,7 +439,7 @@ RiviereQuery instance for the current graph > **serialize**(): `string` -Defined in: [packages/riviere-builder/src/features/building/domain/builder-facade.ts:375](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/builder-facade.ts#L375) +Defined in: packages/riviere-builder/domain-model/src/domain/builder-facade.ts:501 Serializes the current graph state as a JSON string. @@ -453,40 +453,96 @@ JSON string representation of the graph ### stats() -> **stats**(): [`BuilderStats`](../interfaces/BuilderStats.md) +> **stats**(): `object` -Defined in: [packages/riviere-builder/src/features/building/domain/builder-facade.ts:339](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/builder-facade.ts#L339) +Defined in: packages/riviere-builder/domain-model/src/domain/builder-facade.ts:465 Returns statistics about the current graph state. #### Returns -[`BuilderStats`](../interfaces/BuilderStats.md) +`object` Counts of components by type, domains, and links +##### componentCount + +> **componentCount**: `number` = `components.length` + +##### componentsByType + +> **componentsByType**: `object` + +###### componentsByType.API + +> **API**: `number` + +###### componentsByType.Custom + +> **Custom**: `number` + +###### componentsByType.DomainOp + +> **DomainOp**: `number` + +###### componentsByType.Event + +> **Event**: `number` + +###### componentsByType.EventHandler + +> **EventHandler**: `number` + +###### componentsByType.UI + +> **UI**: `number` + +###### componentsByType.UseCase + +> **UseCase**: `number` + +##### domainCount + +> **domainCount**: `number` + +##### externalLinkCount + +> **externalLinkCount**: `number` = `graph.externalLinks.length` + +##### linkCount + +> **linkCount**: `number` = `graph.links.length` + *** ### upsertApi() > **upsertApi**(`input`, `options?`): `object` -Defined in: [packages/riviere-builder/src/features/building/domain/builder-facade.ts:156](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/builder-facade.ts#L156) +Defined in: packages/riviere-builder/domain-model/src/domain/builder-facade.ts:237 + +Adds or updates an API component. #### Parameters ##### input -[`APIInput`](../interfaces/APIInput.md) +`APIInput` + +API component properties ##### options? -[`UpsertOptions`](../interfaces/UpsertOptions.md) +`Readonly`\<\{ `noOverwrite?`: `boolean`; \}\> + +Upsert behaviour #### Returns `object` +The component and whether it was created + ##### component > **component**: `APIComponent` @@ -501,22 +557,30 @@ Defined in: [packages/riviere-builder/src/features/building/domain/builder-facad > **upsertCustom**(`input`, `options?`): `object` -Defined in: [packages/riviere-builder/src/features/building/domain/builder-facade.ts:274](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/builder-facade.ts#L274) +Defined in: packages/riviere-builder/domain-model/src/domain/builder-facade.ts:390 + +Adds or updates a Custom component. #### Parameters ##### input -[`CustomInput`](../interfaces/CustomInput.md) +`CustomInput` + +Custom component properties ##### options? -[`UpsertOptions`](../interfaces/UpsertOptions.md) +`Readonly`\<\{ `noOverwrite?`: `boolean`; \}\> + +Upsert behaviour #### Returns `object` +The component and whether it was created + ##### component > **component**: `CustomComponent` @@ -531,22 +595,30 @@ Defined in: [packages/riviere-builder/src/features/building/domain/builder-facad > **upsertDomainOp**(`input`, `options?`): `object` -Defined in: [packages/riviere-builder/src/features/building/domain/builder-facade.ts:196](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/builder-facade.ts#L196) +Defined in: packages/riviere-builder/domain-model/src/domain/builder-facade.ts:291 + +Adds or updates a DomainOp component. #### Parameters ##### input -[`DomainOpInput`](../interfaces/DomainOpInput.md) +`DomainOpInput` + +DomainOp component properties ##### options? -[`UpsertOptions`](../interfaces/UpsertOptions.md) +`Readonly`\<\{ `noOverwrite?`: `boolean`; \}\> + +Upsert behaviour #### Returns `object` +The component and whether it was created + ##### component > **component**: `DomainOpComponent` @@ -561,22 +633,30 @@ Defined in: [packages/riviere-builder/src/features/building/domain/builder-facad > **upsertEvent**(`input`, `options?`): `object` -Defined in: [packages/riviere-builder/src/features/building/domain/builder-facade.ts:216](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/builder-facade.ts#L216) +Defined in: packages/riviere-builder/domain-model/src/domain/builder-facade.ts:318 + +Adds or updates an Event component. #### Parameters ##### input -[`EventInput`](../interfaces/EventInput.md) +`EventInput` + +Event component properties ##### options? -[`UpsertOptions`](../interfaces/UpsertOptions.md) +`Readonly`\<\{ `noOverwrite?`: `boolean`; \}\> + +Upsert behaviour #### Returns `object` +The component and whether it was created + ##### component > **component**: `EventComponent` @@ -591,22 +671,30 @@ Defined in: [packages/riviere-builder/src/features/building/domain/builder-facad > **upsertEventHandler**(`input`, `options?`): `object` -Defined in: [packages/riviere-builder/src/features/building/domain/builder-facade.ts:236](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/builder-facade.ts#L236) +Defined in: packages/riviere-builder/domain-model/src/domain/builder-facade.ts:345 + +Adds or updates an EventHandler component. #### Parameters ##### input -[`EventHandlerInput`](../interfaces/EventHandlerInput.md) +`EventHandlerInput` + +EventHandler component properties ##### options? -[`UpsertOptions`](../interfaces/UpsertOptions.md) +`Readonly`\<\{ `noOverwrite?`: `boolean`; \}\> + +Upsert behaviour #### Returns `object` +The component and whether it was created + ##### component > **component**: `EventHandlerComponent` @@ -621,22 +709,30 @@ Defined in: [packages/riviere-builder/src/features/building/domain/builder-facad > **upsertUI**(`input`, `options?`): `object` -Defined in: [packages/riviere-builder/src/features/building/domain/builder-facade.ts:136](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/builder-facade.ts#L136) +Defined in: packages/riviere-builder/domain-model/src/domain/builder-facade.ts:210 + +Adds or updates a UI component. #### Parameters ##### input -[`UIInput`](../interfaces/UIInput.md) +`UIInput` + +UI component properties ##### options? -[`UpsertOptions`](../interfaces/UpsertOptions.md) +`Readonly`\<\{ `noOverwrite?`: `boolean`; \}\> + +Upsert behaviour #### Returns `object` +The component and whether it was created + ##### component > **component**: `UIComponent` @@ -651,22 +747,30 @@ Defined in: [packages/riviere-builder/src/features/building/domain/builder-facad > **upsertUseCase**(`input`, `options?`): `object` -Defined in: [packages/riviere-builder/src/features/building/domain/builder-facade.ts:176](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/builder-facade.ts#L176) +Defined in: packages/riviere-builder/domain-model/src/domain/builder-facade.ts:264 + +Adds or updates a UseCase component. #### Parameters ##### input -[`UseCaseInput`](../interfaces/UseCaseInput.md) +`UseCaseInput` + +UseCase component properties ##### options? -[`UpsertOptions`](../interfaces/UpsertOptions.md) +`Readonly`\<\{ `noOverwrite?`: `boolean`; \}\> + +Upsert behaviour #### Returns `object` +The component and whether it was created + ##### component > **component**: `UseCaseComponent` @@ -681,7 +785,7 @@ Defined in: [packages/riviere-builder/src/features/building/domain/builder-facad > **validate**(): `ValidationResult` -Defined in: [packages/riviere-builder/src/features/building/domain/builder-facade.ts:348](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/builder-facade.ts#L348) +Defined in: packages/riviere-builder/domain-model/src/domain/builder-facade.ts:474 Runs full validation on the graph. @@ -695,15 +799,15 @@ Validation result with valid flag and error details ### warnings() -> **warnings**(): [`BuilderWarning`](../interfaces/BuilderWarning.md)[] +> **warnings**(): (`InspectionWarning` \| `OperationWarning`)[] -Defined in: [packages/riviere-builder/src/features/building/domain/builder-facade.ts:330](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/builder-facade.ts#L330) +Defined in: packages/riviere-builder/domain-model/src/domain/builder-facade.ts:456 Returns non-fatal issues found in the graph. #### Returns -[`BuilderWarning`](../interfaces/BuilderWarning.md)[] +(`InspectionWarning` \| `OperationWarning`)[] Array of warning objects with type and message @@ -713,7 +817,7 @@ Array of warning objects with type and message > `static` **new**(`options`, `graphPath`): `RiviereBuilder` -Defined in: [packages/riviere-builder/src/features/building/domain/builder-facade.ts:104](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/builder-facade.ts#L104) +Defined in: packages/riviere-builder/domain-model/src/domain/builder-facade.ts:171 Creates a new builder with initial configuration. @@ -721,7 +825,7 @@ Creates a new builder with initial configuration. ##### options -[`BuilderOptions`](../interfaces/BuilderOptions.md) +`BuilderOptions` Configuration including sources and domains @@ -743,7 +847,7 @@ A new RiviereBuilder instance > `static` **resume**(`graph`, `graphPath`): `RiviereBuilder` -Defined in: [packages/riviere-builder/src/features/building/domain/builder-facade.ts:93](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/builder-facade.ts#L93) +Defined in: packages/riviere-builder/domain-model/src/domain/builder-facade.ts:160 Restores a builder from a previously serialized graph. diff --git a/apps/docs/reference/api/generated/riviere-builder/classes/RiviereQuery.md b/apps/docs/reference/api/generated/riviere-builder/classes/RiviereQuery.md new file mode 100644 index 000000000..9389ad325 --- /dev/null +++ b/apps/docs/reference/api/generated/riviere-builder/classes/RiviereQuery.md @@ -0,0 +1,980 @@ +--- +pageClass: reference +--- + +# Class: RiviereQuery + +Defined in: packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.ts:86 + +Query and analyze Riviere architecture graphs. + +RiviereQuery provides methods to explore components, trace execution flows, +analyze domain models, and compare graph versions. + +## Example + +```typescript +import { RiviereQuery } from '@living-architecture/riviere-builder-domain-model/query' + +// From JSON +const query = RiviereQuery.fromJSON(graphData) + +// Query components +const apis = query.componentsByType('API') +const orderDomain = query.componentsInDomain('orders') + +// Trace flows +const flow = query.traceFlow('orders:checkout:api:post-orders') +``` + +## Riviere-role + +domain-service + +## Constructors + +### Constructor + +> **new RiviereQuery**(`graph`): `RiviereQuery` + +Defined in: packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.ts:101 + +Creates a new RiviereQuery instance. + +#### Parameters + +##### graph + +`RiviereGraph` + +A valid RiviereGraph object + +#### Returns + +`RiviereQuery` + +#### Throws + +If the graph fails schema validation + +#### Example + +```typescript +const graph: RiviereGraph = JSON.parse(jsonString) +const query = new RiviereQuery(graph) +``` + +## Methods + +### businessRulesFor() + +> **businessRulesFor**(`entityName`): `string`[] + +Defined in: packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.ts:349 + +Returns all business rules for an entity's operations. + +#### Parameters + +##### entityName + +`string` + +The entity name to get rules for + +#### Returns + +`string`[] + +Array of business rule strings + +#### Example + +```typescript +const rules = query.businessRulesFor('Order') +``` + +*** + +### componentById() + +> **componentById**(`id`): `Component` \| `undefined` + +Defined in: packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.ts:233 + +Finds a component by its ID. + +#### Parameters + +##### id + +`ComponentId` + +The component ID to look up + +#### Returns + +`Component` \| `undefined` + +The component, or undefined if not found + +#### Example + +```typescript +const component = query.componentById('orders:checkout:api:post-orders') +``` + +*** + +### components() + +> **components**(): `Component`[] + +Defined in: packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.ts:135 + +Returns all components in the graph. + +#### Returns + +`Component`[] + +Array of all components + +#### Example + +```typescript +const allComponents = query.components() +console.log(`Total: ${allComponents.length}`) +``` + +*** + +### componentsByType() + +> **componentsByType**(`type`): `Component`[] + +Defined in: packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.ts:282 + +Returns all components of a specific type. + +#### Parameters + +##### type + +`ComponentType` + +The component type to filter by + +#### Returns + +`Component`[] + +Array of components of that type + +#### Example + +```typescript +const apis = query.componentsByType('API') +const events = query.componentsByType('Event') +``` + +*** + +### componentsInDomain() + +> **componentsInDomain**(`domainName`): `Component`[] + +Defined in: packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.ts:266 + +Returns all components in a specific domain. + +#### Parameters + +##### domainName + +`string` + +The domain name to filter by + +#### Returns + +`Component`[] + +Array of components in the domain + +#### Example + +```typescript +const orderComponents = query.componentsInDomain('orders') +``` + +*** + +### crossDomainLinks() + +> **crossDomainLinks**(`domainName`): `CrossDomainLink`[] + +Defined in: packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.ts:536 + +Returns links from a domain to other domains. + +#### Parameters + +##### domainName + +`string` + +The source domain name + +#### Returns + +`CrossDomainLink`[] + +Array of CrossDomainLink objects (deduplicated by target domain and type) + +#### Example + +```typescript +const outgoing = query.crossDomainLinks('orders') +``` + +*** + +### detectOrphans() + +> **detectOrphans**(): `ComponentId`[] + +Defined in: packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.ts:186 + +Detects orphan components with no incoming or outgoing links. + +#### Returns + +`ComponentId`[] + +Array of component IDs that are disconnected from the graph + +#### Example + +```typescript +const orphanIds = query.detectOrphans() +if (orphanIds.length > 0) { + console.warn(`Found ${orphanIds.length} orphan nodes`) +} +``` + +*** + +### diff() + +> **diff**(`other`): `GraphDiff` + +Defined in: packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.ts:441 + +Compares this graph with another and returns the differences. + +#### Parameters + +##### other + +`RiviereGraph` + +The graph to compare against + +#### Returns + +`GraphDiff` + +GraphDiff with added, removed, and modified items + +#### Example + +```typescript +const oldGraph = RiviereQuery.fromJSON(oldData) +const newGraph = RiviereQuery.fromJSON(newData) +const diff = newGraph.diff(oldGraph.graph) + +console.log(`Added: ${diff.stats.componentsAdded}`) +console.log(`Removed: ${diff.stats.componentsRemoved}`) +``` + +*** + +### domainConnections() + +> **domainConnections**(`domainName`): `DomainConnection`[] + +Defined in: packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.ts:556 + +Returns cross-domain connections with API and event counts. + +Shows both incoming and outgoing connections for a domain. + +#### Parameters + +##### domainName + +`string` + +The domain to analyze + +#### Returns + +`DomainConnection`[] + +Array of DomainConnection objects + +#### Example + +```typescript +const connections = query.domainConnections('orders') +for (const conn of connections) { + console.log(`${conn.direction} to ${conn.targetDomain}: ${conn.apiCount} API, ${conn.eventCount} event`) +} +``` + +*** + +### domains() + +> **domains**(): `Domain`[] + +Defined in: packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.ts:299 + +Returns domain information with component counts. + +#### Returns + +`Domain`[] + +Array of Domain objects sorted by name + +#### Example + +```typescript +const domains = query.domains() +for (const domain of domains) { + console.log(`${domain.name}: ${domain.componentCounts.total} components`) +} +``` + +*** + +### entities() + +> **entities**(`domainName?`): `Entity`[] + +Defined in: packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.ts:334 + +Returns entities with their domain operations. + +#### Parameters + +##### domainName? + +`string` + +Optional domain to filter by + +#### Returns + +`Entity`[] + +Array of Entity objects with their operations + +#### Example + +```typescript +const allEntities = query.entities() +const orderEntities = query.entities('orders') + +for (const entity of orderEntities) { + console.log(`${entity.name} has ${entity.operations.length} operations`) +} +``` + +*** + +### entryPoints() + +> **entryPoints**(): `Component`[] + +Defined in: packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.ts:399 + +Returns components that are entry points to the system. + +Entry points are UI, API, EventHandler, or Custom components +with no incoming links. + +#### Returns + +`Component`[] + +Array of entry point components + +#### Example + +```typescript +const entryPoints = query.entryPoints() +``` + +*** + +### eventHandlers() + +> **eventHandlers**(`eventName?`): `EventHandlerInfo`[] + +Defined in: packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.ts:477 + +Returns event handlers with their subscriptions. + +#### Parameters + +##### eventName? + +`string` + +Optional event name to filter handlers by + +#### Returns + +`EventHandlerInfo`[] + +Array of EventHandlerInfo objects sorted by handler name + +#### Example + +```typescript +const allHandlers = query.eventHandlers() +const orderPlacedHandlers = query.eventHandlers('order-placed') +``` + +*** + +### externalDomains() + +> **externalDomains**(): `ExternalDomain`[] + +Defined in: packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.ts:632 + +Returns external domains that components connect to. + +Each unique external target is returned as a separate ExternalDomain, +with aggregated source domains and connection counts. + +#### Returns + +`ExternalDomain`[] + +Array of ExternalDomain objects, sorted alphabetically by name + +#### Example + +```typescript +const externals = query.externalDomains() +for (const ext of externals) { + console.log(`${ext.name}: ${ext.connectionCount} connections from ${ext.sourceDomains.join(', ')}`) +} +``` + +*** + +### externalLinks() + +> **externalLinks**(): `ExternalLink`[] + +Defined in: packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.ts:612 + +Returns all external links in the graph. + +External links represent connections from components to external +systems that are not part of the graph (e.g., third-party APIs). + +#### Returns + +`ExternalLink`[] + +Array of all external links, or empty array if none exist + +#### Example + +```typescript +const externalLinks = query.externalLinks() +for (const link of externalLinks) { + console.log(`${link.source} -> ${link.target.name}`) +} +``` + +*** + +### find() + +> **find**(`predicate`): `Component` \| `undefined` + +Defined in: packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.ts:201 + +Finds the first component matching a predicate. + +#### Parameters + +##### predicate + +(`component`) => `boolean` + +Function that returns true for matching components + +#### Returns + +`Component` \| `undefined` + +The first matching component, or undefined if none found + +#### Example + +```typescript +const checkout = query.find(c => c.name.includes('checkout')) +``` + +*** + +### findAll() + +> **findAll**(`predicate`): `Component`[] + +Defined in: packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.ts:218 + +Finds all components matching a predicate. + +#### Parameters + +##### predicate + +(`component`) => `boolean` + +Function that returns true for matching components + +#### Returns + +`Component`[] + +Array of all matching components + +#### Example + +```typescript +const orderHandlers = query.findAll(c => + c.type === 'EventHandler' && c.domain === 'orders' +) +``` + +*** + +### flows() + +> **flows**(): `Flow`[] + +Defined in: packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.ts:501 + +Returns all flows in the graph. + +Each flow starts from an entry point (UI, API, or Custom with no +incoming links) and traces forward through the graph. + +#### Returns + +`Flow`[] + +Array of Flow objects with entry point and steps + +#### Example + +```typescript +const flows = query.flows() + +for (const flow of flows) { + console.log(`Flow: ${flow.entryPoint.name}`) + for (const step of flow.steps) { + console.log(` ${step.component.name} (depth: ${step.depth})`) + } +} +``` + +*** + +### links() + +> **links**(): `Link`[] + +Defined in: packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.ts:150 + +Returns all links in the graph. + +#### Returns + +`Link`[] + +Array of all links + +#### Example + +```typescript +const allLinks = query.links() +console.log(`Total links: ${allLinks.length}`) +``` + +*** + +### nodeDepths() + +> **nodeDepths**(): `ComponentDepths` + +Defined in: packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.ts:592 + +Calculates depth from entry points for each component. + +Components unreachable from entry points will not be in the map. + +#### Returns + +`ComponentDepths` + +Map of component ID to depth (0 = entry point) + +#### Example + +```typescript +const depths = query.nodeDepths() +for (const [id, depth] of depths) { + console.log(`${id}: depth ${depth}`) +} +``` + +*** + +### operationsFor() + +> **operationsFor**(`entityName`): `DomainOpComponent`[] + +Defined in: packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.ts:314 + +Returns all domain operations for a specific entity. + +#### Parameters + +##### entityName + +`string` + +The entity name to get operations for + +#### Returns + +`DomainOpComponent`[] + +Array of DomainOp components targeting the entity + +#### Example + +```typescript +const orderOps = query.operationsFor('Order') +``` + +*** + +### publishedEvents() + +> **publishedEvents**(`domainName?`): `PublishedEvent`[] + +Defined in: packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.ts:461 + +Returns published events with their handlers. + +#### Parameters + +##### domainName? + +`string` + +Optional domain to filter by + +#### Returns + +`PublishedEvent`[] + +Array of PublishedEvent objects sorted by event name + +#### Example + +```typescript +const allEvents = query.publishedEvents() +const orderEvents = query.publishedEvents('orders') + +for (const event of orderEvents) { + console.log(`${event.eventName} has ${event.handlers.length} handlers`) +} +``` + +*** + +### search() + +> **search**(`query`): `Component`[] + +Defined in: packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.ts:251 + +Searches components by name, domain, or type. + +Case-insensitive search across component name, domain, and type fields. + +#### Parameters + +##### query + +`string` + +Search term + +#### Returns + +`Component`[] + +Array of matching components + +#### Example + +```typescript +const results = query.search('order') +// Matches: "PlaceOrder", "orders" domain, etc. +``` + +*** + +### searchWithFlow() + +> **searchWithFlow**(`query`, `options`): `SearchWithFlowResult` + +Defined in: packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.ts:521 + +Searches for components and returns their flow context. + +Returns both matching component IDs and all visible IDs in their flows. + +#### Parameters + +##### query + +`string` + +Search term + +##### options + +`SearchWithFlowOptions` + +Search options including returnAllOnEmptyQuery + +#### Returns + +`SearchWithFlowResult` + +Object with matchingIds and visibleIds arrays + +#### Example + +```typescript +const result = query.searchWithFlow('checkout', { returnAllOnEmptyQuery: true }) +console.log(`Found ${result.matchingIds.length} matches`) +console.log(`Showing ${result.visibleIds.length} nodes in context`) +``` + +*** + +### statesFor() + +> **statesFor**(`entityName`): `State`[] + +Defined in: packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.ts:382 + +Returns ordered states for an entity based on transitions. + +States are ordered by transition flow from initial to final states. + +#### Parameters + +##### entityName + +`string` + +The entity name to get states for + +#### Returns + +`State`[] + +Array of state names in transition order + +#### Example + +```typescript +const orderStates = query.statesFor('Order') +// ['pending', 'confirmed', 'shipped', 'delivered'] +``` + +*** + +### stats() + +> **stats**(): `GraphStats` + +Defined in: packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.ts:573 + +Returns aggregate statistics about the graph. + +#### Returns + +`GraphStats` + +GraphStats with counts for components, links, domains, APIs, entities, and events + +#### Example + +```typescript +const stats = query.stats() +console.log(`Components: ${stats.componentCount}`) +console.log(`Links: ${stats.linkCount}`) +console.log(`Domains: ${stats.domainCount}`) +``` + +*** + +### traceFlow() + +> **traceFlow**(`startComponentId`): `object` + +Defined in: packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.ts:418 + +Traces the complete flow bidirectionally from a starting component. + +Returns all nodes and links connected to the starting point, +following links in both directions. + +#### Parameters + +##### startComponentId + +`ComponentId` + +ID of the component to start tracing from + +#### Returns + +`object` + +Object with componentIds and linkIds in the flow + +##### componentIds + +> **componentIds**: `ComponentId`[] + +##### linkIds + +> **linkIds**: `LinkId`[] + +#### Example + +```typescript +const flow = query.traceFlow('orders:checkout:api:post-orders') +console.log(`Flow includes ${flow.componentIds.length} nodes`) +``` + +*** + +### transitionsFor() + +> **transitionsFor**(`entityName`): `EntityTransition`[] + +Defined in: packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.ts:364 + +Returns state transitions for an entity. + +#### Parameters + +##### entityName + +`string` + +The entity name to get transitions for + +#### Returns + +`EntityTransition`[] + +Array of EntityTransition objects + +#### Example + +```typescript +const transitions = query.transitionsFor('Order') +``` + +*** + +### validate() + +> **validate**(): `ValidationResult` + +Defined in: packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.ts:169 + +Validates the graph structure beyond schema validation. + +Checks for structural issues like invalid link references. + +#### Returns + +`ValidationResult` + +Validation result with any errors found + +#### Example + +```typescript +const result = query.validate() +if (!result.valid) { + console.error('Validation errors:', result.errors) +} +``` + +*** + +### fromJSON() + +> `static` **fromJSON**(`json`): `RiviereQuery` + +Defined in: packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.ts:119 + +Creates a RiviereQuery from raw JSON data. + +#### Parameters + +##### json + +`unknown` + +Raw JSON data to parse as a RiviereGraph + +#### Returns + +`RiviereQuery` + +A new RiviereQuery instance + +#### Throws + +If the JSON fails schema validation + +#### Example + +```typescript +const jsonData = await fetch('/graph.json').then(r => r.json()) +const query = RiviereQuery.fromJSON(jsonData) +``` diff --git a/apps/docs/reference/api/generated/riviere-builder/classes/SourceConflictError.md b/apps/docs/reference/api/generated/riviere-builder/classes/SourceConflictError.md index 35d9ee01e..0ced2caec 100644 --- a/apps/docs/reference/api/generated/riviere-builder/classes/SourceConflictError.md +++ b/apps/docs/reference/api/generated/riviere-builder/classes/SourceConflictError.md @@ -4,7 +4,7 @@ pageClass: reference # Class: SourceConflictError -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:13](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L13) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:13 ## Riviere-role @@ -20,7 +20,7 @@ domain-error > **new SourceConflictError**(`repository`): `SourceConflictError` -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:16](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L16) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:16 #### Parameters @@ -78,7 +78,7 @@ Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/li > `readonly` **repository**: `string` -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts:14](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts#L14) +Defined in: packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts:14 *** diff --git a/apps/docs/reference/api/generated/riviere-builder/functions/findNearMatches.md b/apps/docs/reference/api/generated/riviere-builder/functions/findNearMatches.md index a245ebd0f..a5a00024b 100644 --- a/apps/docs/reference/api/generated/riviere-builder/functions/findNearMatches.md +++ b/apps/docs/reference/api/generated/riviere-builder/functions/findNearMatches.md @@ -4,9 +4,9 @@ pageClass: reference # Function: findNearMatches() -> **findNearMatches**(`components`, `query`, `options?`): [`NearMatchResult`](../interfaces/NearMatchResult.md)[] +> **findNearMatches**(`components`, `query`, `options?`): `Readonly`\<\{ `component`: `Component`; `mismatch?`: `Readonly`\<\{ `actual`: `string`; `expected`: `string`; `field`: `"type"` \| `"domain"`; \}\>; `score`: `number`; \}\>[] -Defined in: [packages/riviere-builder/src/features/building/domain/error-recovery/component-suggestion.ts:60](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/error-recovery/component-suggestion.ts#L60) +Defined in: packages/riviere-builder/domain-model/src/domain/error-recovery/component-suggestion.ts:75 Finds components similar to a query using fuzzy matching. @@ -16,25 +16,25 @@ Used for error recovery to suggest alternatives when exact matches fail. ### components -`Component`[] +readonly `Component`[] Array of components to search ### query -[`NearMatchQuery`](../interfaces/NearMatchQuery.md) +`NearMatchQuery` Search criteria with name and optional type/domain filters ### options? -[`NearMatchOptions`](../interfaces/NearMatchOptions.md) +`Readonly`\<\{ `limit?`: `number`; `threshold?`: `number`; \}\> Optional threshold and limit settings ## Returns -[`NearMatchResult`](../interfaces/NearMatchResult.md)[] +`Readonly`\<\{ `component`: `Component`; `mismatch?`: `Readonly`\<\{ `actual`: `string`; `expected`: `string`; `field`: `"type"` \| `"domain"`; \}\>; `score`: `number`; \}\>[] Array of matching components with similarity scores diff --git a/apps/docs/reference/api/generated/riviere-builder/interfaces/APIInput.md b/apps/docs/reference/api/generated/riviere-builder/interfaces/APIInput.md deleted file mode 100644 index 2be0191b2..000000000 --- a/apps/docs/reference/api/generated/riviere-builder/interfaces/APIInput.md +++ /dev/null @@ -1,91 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: APIInput - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:44](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L44) - -## Riviere-role - -value-object - -## Properties - -### apiType - -> **apiType**: `ApiType` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:48](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L48) - -*** - -### description? - -> `optional` **description**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:52](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L52) - -*** - -### domain - -> **domain**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:46](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L46) - -*** - -### httpMethod? - -> `optional` **httpMethod**: `HttpMethod` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:49](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L49) - -*** - -### metadata? - -> `optional` **metadata**: `Record`\<`string`, `unknown`\> - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:54](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L54) - -*** - -### module - -> **module**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:47](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L47) - -*** - -### name - -> **name**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:45](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L45) - -*** - -### operationName? - -> `optional` **operationName**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:51](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L51) - -*** - -### path? - -> `optional` **path**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:50](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L50) - -*** - -### sourceLocation - -> **sourceLocation**: `SourceLocation` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:53](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L53) diff --git a/apps/docs/reference/api/generated/riviere-builder/interfaces/BuilderOptions.md b/apps/docs/reference/api/generated/riviere-builder/interfaces/BuilderOptions.md deleted file mode 100644 index 264c3a06c..000000000 --- a/apps/docs/reference/api/generated/riviere-builder/interfaces/BuilderOptions.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: BuilderOptions - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:15](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L15) - -## Riviere-role - -value-object - -## Properties - -### description? - -> `optional` **description**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:17](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L17) - -*** - -### domains - -> **domains**: `Record`\<`string`, `DomainMetadata`\> - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:19](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L19) - -*** - -### name? - -> `optional` **name**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:16](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L16) - -*** - -### sources - -> **sources**: `SourceInfo`[] - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:18](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L18) diff --git a/apps/docs/reference/api/generated/riviere-builder/interfaces/BuilderStats.md b/apps/docs/reference/api/generated/riviere-builder/interfaces/BuilderStats.md deleted file mode 100644 index 095450477..000000000 --- a/apps/docs/reference/api/generated/riviere-builder/interfaces/BuilderStats.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: BuilderStats - -Defined in: [packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts:2](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts#L2) - -## Riviere-role - -value-object - -## Properties - -### componentCount - -> **componentCount**: `number` - -Defined in: [packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts:3](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts#L3) - -*** - -### componentsByType - -> **componentsByType**: `object` - -Defined in: [packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts:4](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts#L4) - -#### API - -> **API**: `number` - -#### Custom - -> **Custom**: `number` - -#### DomainOp - -> **DomainOp**: `number` - -#### Event - -> **Event**: `number` - -#### EventHandler - -> **EventHandler**: `number` - -#### UI - -> **UI**: `number` - -#### UseCase - -> **UseCase**: `number` - -*** - -### domainCount - -> **domainCount**: `number` - -Defined in: [packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts:15](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts#L15) - -*** - -### externalLinkCount - -> **externalLinkCount**: `number` - -Defined in: [packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts:14](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts#L14) - -*** - -### linkCount - -> **linkCount**: `number` - -Defined in: [packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts:13](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts#L13) diff --git a/apps/docs/reference/api/generated/riviere-builder/interfaces/BuilderWarning.md b/apps/docs/reference/api/generated/riviere-builder/interfaces/BuilderWarning.md deleted file mode 100644 index afbeb925a..000000000 --- a/apps/docs/reference/api/generated/riviere-builder/interfaces/BuilderWarning.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: BuilderWarning - -Defined in: [packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts:26](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts#L26) - -## Riviere-role - -value-object - -## Properties - -### code - -> **code**: `WarningCode` - -Defined in: [packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts:27](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts#L27) - -*** - -### componentId? - -> `optional` **componentId**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts:29](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts#L29) - -*** - -### domainName? - -> `optional` **domainName**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts:30](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts#L30) - -*** - -### field? - -> `optional` **field**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts:31](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts#L31) - -*** - -### linkType? - -> `optional` **linkType**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts:36](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts#L36) - -*** - -### message - -> **message**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts:28](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts#L28) - -*** - -### newValue? - -> `optional` **newValue**: `string` \| `number` \| `boolean` - -Defined in: [packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts:33](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts#L33) - -*** - -### oldValue? - -> `optional` **oldValue**: `string` \| `number` \| `boolean` - -Defined in: [packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts:32](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts#L32) - -*** - -### source? - -> `optional` **source**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts:34](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts#L34) - -*** - -### target? - -> `optional` **target**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts:35](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts#L35) - -*** - -### targetName? - -> `optional` **targetName**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts:38](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts#L38) - -*** - -### targetRepository? - -> `optional` **targetRepository**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts:37](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts#L37) diff --git a/apps/docs/reference/api/generated/riviere-builder/interfaces/ComponentIdParts.md b/apps/docs/reference/api/generated/riviere-builder/interfaces/ComponentIdParts.md deleted file mode 100644 index 031d95eea..000000000 --- a/apps/docs/reference/api/generated/riviere-builder/interfaces/ComponentIdParts.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: ComponentIdParts - -Defined in: packages/riviere-schema/dist/component-id.d.ts:4 - -Parts that make up a component ID. - -## Properties - -### domain - -> **domain**: `string` - -Defined in: packages/riviere-schema/dist/component-id.d.ts:5 - -*** - -### module - -> **module**: `string` - -Defined in: packages/riviere-schema/dist/component-id.d.ts:6 - -*** - -### name - -> **name**: `string` - -Defined in: packages/riviere-schema/dist/component-id.d.ts:8 - -*** - -### type - -> **type**: `string` - -Defined in: packages/riviere-schema/dist/component-id.d.ts:7 diff --git a/apps/docs/reference/api/generated/riviere-builder/interfaces/CustomInput.md b/apps/docs/reference/api/generated/riviere-builder/interfaces/CustomInput.md deleted file mode 100644 index 814d0d8df..000000000 --- a/apps/docs/reference/api/generated/riviere-builder/interfaces/CustomInput.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: CustomInput - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:121](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L121) - -## Riviere-role - -value-object - -## Properties - -### customTypeName - -> **customTypeName**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:122](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L122) - -*** - -### description? - -> `optional` **description**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:126](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L126) - -*** - -### domain - -> **domain**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:124](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L124) - -*** - -### metadata? - -> `optional` **metadata**: `Record`\<`string`, `unknown`\> - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:128](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L128) - -*** - -### module - -> **module**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:125](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L125) - -*** - -### name - -> **name**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:123](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L123) - -*** - -### sourceLocation - -> **sourceLocation**: `SourceLocation` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:127](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L127) diff --git a/apps/docs/reference/api/generated/riviere-builder/interfaces/CustomTypeInput.md b/apps/docs/reference/api/generated/riviere-builder/interfaces/CustomTypeInput.md deleted file mode 100644 index 0884b4a8d..000000000 --- a/apps/docs/reference/api/generated/riviere-builder/interfaces/CustomTypeInput.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: CustomTypeInput - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:107](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L107) - -## Riviere-role - -value-object - -## Properties - -### description? - -> `optional` **description**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:109](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L109) - -*** - -### name - -> **name**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:108](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L108) - -*** - -### optionalProperties? - -> `optional` **optionalProperties**: `Record`\<`string`, `CustomPropertyDefinition`\> - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:111](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L111) - -*** - -### requiredProperties? - -> `optional` **requiredProperties**: `Record`\<`string`, `CustomPropertyDefinition`\> - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:110](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L110) diff --git a/apps/docs/reference/api/generated/riviere-builder/interfaces/DomainInput.md b/apps/docs/reference/api/generated/riviere-builder/interfaces/DomainInput.md deleted file mode 100644 index b45237294..000000000 --- a/apps/docs/reference/api/generated/riviere-builder/interfaces/DomainInput.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: DomainInput - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:23](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L23) - -## Riviere-role - -value-object - -## Properties - -### description - -> **description**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:25](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L25) - -*** - -### name - -> **name**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:24](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L24) - -*** - -### systemType - -> **systemType**: `SystemType` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:26](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L26) diff --git a/apps/docs/reference/api/generated/riviere-builder/interfaces/DomainOpInput.md b/apps/docs/reference/api/generated/riviere-builder/interfaces/DomainOpInput.md deleted file mode 100644 index 4c182db82..000000000 --- a/apps/docs/reference/api/generated/riviere-builder/interfaces/DomainOpInput.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: DomainOpInput - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:68](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L68) - -## Riviere-role - -value-object - -## Properties - -### behavior? - -> `optional` **behavior**: `OperationBehavior` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:75](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L75) - -*** - -### businessRules? - -> `optional` **businessRules**: `string`[] - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:77](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L77) - -*** - -### description? - -> `optional` **description**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:78](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L78) - -*** - -### domain - -> **domain**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:70](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L70) - -*** - -### entity? - -> `optional` **entity**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:73](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L73) - -*** - -### metadata? - -> `optional` **metadata**: `Record`\<`string`, `unknown`\> - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:80](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L80) - -*** - -### module - -> **module**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:71](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L71) - -*** - -### name - -> **name**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:69](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L69) - -*** - -### operationName - -> **operationName**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:72](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L72) - -*** - -### signature? - -> `optional` **signature**: `OperationSignature` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:74](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L74) - -*** - -### sourceLocation - -> **sourceLocation**: `SourceLocation` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:79](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L79) - -*** - -### stateChanges? - -> `optional` **stateChanges**: `StateTransition`[] - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:76](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L76) diff --git a/apps/docs/reference/api/generated/riviere-builder/interfaces/EnrichmentInput.md b/apps/docs/reference/api/generated/riviere-builder/interfaces/EnrichmentInput.md deleted file mode 100644 index 24a430d45..000000000 --- a/apps/docs/reference/api/generated/riviere-builder/interfaces/EnrichmentInput.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: EnrichmentInput - -Defined in: [packages/riviere-builder/src/features/building/domain/enrichment/enrichment-types.ts:8](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/enrichment/enrichment-types.ts#L8) - -## Riviere-role - -value-object - -## Properties - -### behavior? - -> `optional` **behavior**: `OperationBehavior` - -Defined in: [packages/riviere-builder/src/features/building/domain/enrichment/enrichment-types.ts:12](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/enrichment/enrichment-types.ts#L12) - -*** - -### businessRules? - -> `optional` **businessRules**: `string`[] - -Defined in: [packages/riviere-builder/src/features/building/domain/enrichment/enrichment-types.ts:11](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/enrichment/enrichment-types.ts#L11) - -*** - -### entity? - -> `optional` **entity**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/enrichment/enrichment-types.ts:9](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/enrichment/enrichment-types.ts#L9) - -*** - -### signature? - -> `optional` **signature**: `OperationSignature` - -Defined in: [packages/riviere-builder/src/features/building/domain/enrichment/enrichment-types.ts:13](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/enrichment/enrichment-types.ts#L13) - -*** - -### stateChanges? - -> `optional` **stateChanges**: `StateTransition`[] - -Defined in: [packages/riviere-builder/src/features/building/domain/enrichment/enrichment-types.ts:10](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/enrichment/enrichment-types.ts#L10) diff --git a/apps/docs/reference/api/generated/riviere-builder/interfaces/EventHandlerInput.md b/apps/docs/reference/api/generated/riviere-builder/interfaces/EventHandlerInput.md deleted file mode 100644 index fe39c8a82..000000000 --- a/apps/docs/reference/api/generated/riviere-builder/interfaces/EventHandlerInput.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: EventHandlerInput - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:96](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L96) - -## Riviere-role - -value-object - -## Properties - -### description? - -> `optional` **description**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:101](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L101) - -*** - -### domain - -> **domain**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:98](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L98) - -*** - -### metadata? - -> `optional` **metadata**: `Record`\<`string`, `unknown`\> - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:103](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L103) - -*** - -### module - -> **module**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:99](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L99) - -*** - -### name - -> **name**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:97](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L97) - -*** - -### sourceLocation - -> **sourceLocation**: `SourceLocation` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:102](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L102) - -*** - -### subscribedEvents - -> **subscribedEvents**: `string`[] - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:100](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L100) diff --git a/apps/docs/reference/api/generated/riviere-builder/interfaces/EventInput.md b/apps/docs/reference/api/generated/riviere-builder/interfaces/EventInput.md deleted file mode 100644 index ca416552c..000000000 --- a/apps/docs/reference/api/generated/riviere-builder/interfaces/EventInput.md +++ /dev/null @@ -1,75 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: EventInput - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:84](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L84) - -## Riviere-role - -value-object - -## Properties - -### description? - -> `optional` **description**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:90](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L90) - -*** - -### domain - -> **domain**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:86](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L86) - -*** - -### eventName - -> **eventName**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:88](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L88) - -*** - -### eventSchema? - -> `optional` **eventSchema**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:89](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L89) - -*** - -### metadata? - -> `optional` **metadata**: `Record`\<`string`, `unknown`\> - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:92](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L92) - -*** - -### module - -> **module**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:87](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L87) - -*** - -### name - -> **name**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:85](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L85) - -*** - -### sourceLocation - -> **sourceLocation**: `SourceLocation` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:91](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L91) diff --git a/apps/docs/reference/api/generated/riviere-builder/interfaces/ExternalLinkInput.md b/apps/docs/reference/api/generated/riviere-builder/interfaces/ExternalLinkInput.md deleted file mode 100644 index dc3bc1292..000000000 --- a/apps/docs/reference/api/generated/riviere-builder/interfaces/ExternalLinkInput.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: ExternalLinkInput - -Defined in: [packages/riviere-builder/src/features/building/domain/linking/linking-types.ts:16](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/linking/linking-types.ts#L16) - -## Riviere-role - -value-object - -## Properties - -### description? - -> `optional` **description**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/linking/linking-types.ts:20](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/linking/linking-types.ts#L20) - -*** - -### from - -> **from**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/linking/linking-types.ts:17](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/linking/linking-types.ts#L17) - -*** - -### metadata? - -> `optional` **metadata**: `Record`\<`string`, `unknown`\> - -Defined in: [packages/riviere-builder/src/features/building/domain/linking/linking-types.ts:22](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/linking/linking-types.ts#L22) - -*** - -### sourceLocation? - -> `optional` **sourceLocation**: `SourceLocation` - -Defined in: [packages/riviere-builder/src/features/building/domain/linking/linking-types.ts:21](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/linking/linking-types.ts#L21) - -*** - -### target - -> **target**: `ExternalTarget` - -Defined in: [packages/riviere-builder/src/features/building/domain/linking/linking-types.ts:18](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/linking/linking-types.ts#L18) - -*** - -### type? - -> `optional` **type**: `LinkType` - -Defined in: [packages/riviere-builder/src/features/building/domain/linking/linking-types.ts:19](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/linking/linking-types.ts#L19) diff --git a/apps/docs/reference/api/generated/riviere-builder/interfaces/LinkInput.md b/apps/docs/reference/api/generated/riviere-builder/interfaces/LinkInput.md deleted file mode 100644 index 4c31f5e16..000000000 --- a/apps/docs/reference/api/generated/riviere-builder/interfaces/LinkInput.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: LinkInput - -Defined in: [packages/riviere-builder/src/features/building/domain/linking/linking-types.ts:6](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/linking/linking-types.ts#L6) - -## Riviere-role - -value-object - -## Properties - -### condition? - -> `optional` **condition**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/linking/linking-types.ts:11](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/linking/linking-types.ts#L11) - -*** - -### from - -> **from**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/linking/linking-types.ts:7](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/linking/linking-types.ts#L7) - -*** - -### relationshipType? - -> `optional` **relationshipType**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/linking/linking-types.ts:10](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/linking/linking-types.ts#L10) - -*** - -### sourceLocation? - -> `optional` **sourceLocation**: `SourceLocation` - -Defined in: [packages/riviere-builder/src/features/building/domain/linking/linking-types.ts:12](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/linking/linking-types.ts#L12) - -*** - -### to - -> **to**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/linking/linking-types.ts:8](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/linking/linking-types.ts#L8) - -*** - -### type? - -> `optional` **type**: `LinkType` - -Defined in: [packages/riviere-builder/src/features/building/domain/linking/linking-types.ts:9](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/linking/linking-types.ts#L9) diff --git a/apps/docs/reference/api/generated/riviere-builder/interfaces/NearMatchMismatch.md b/apps/docs/reference/api/generated/riviere-builder/interfaces/NearMatchMismatch.md deleted file mode 100644 index 481b52e09..000000000 --- a/apps/docs/reference/api/generated/riviere-builder/interfaces/NearMatchMismatch.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: NearMatchMismatch - -Defined in: [packages/riviere-builder/src/features/building/domain/error-recovery/match-types.ts:11](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/error-recovery/match-types.ts#L11) - -## Riviere-role - -value-object - -## Properties - -### actual - -> **actual**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/error-recovery/match-types.ts:14](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/error-recovery/match-types.ts#L14) - -*** - -### expected - -> **expected**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/error-recovery/match-types.ts:13](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/error-recovery/match-types.ts#L13) - -*** - -### field - -> **field**: `"domain"` \| `"type"` - -Defined in: [packages/riviere-builder/src/features/building/domain/error-recovery/match-types.ts:12](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/error-recovery/match-types.ts#L12) diff --git a/apps/docs/reference/api/generated/riviere-builder/interfaces/NearMatchOptions.md b/apps/docs/reference/api/generated/riviere-builder/interfaces/NearMatchOptions.md deleted file mode 100644 index 65e895cb8..000000000 --- a/apps/docs/reference/api/generated/riviere-builder/interfaces/NearMatchOptions.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: NearMatchOptions - -Defined in: [packages/riviere-builder/src/features/building/domain/error-recovery/match-types.ts:25](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/error-recovery/match-types.ts#L25) - -## Riviere-role - -value-object - -## Properties - -### limit? - -> `optional` **limit**: `number` - -Defined in: [packages/riviere-builder/src/features/building/domain/error-recovery/match-types.ts:27](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/error-recovery/match-types.ts#L27) - -*** - -### threshold? - -> `optional` **threshold**: `number` - -Defined in: [packages/riviere-builder/src/features/building/domain/error-recovery/match-types.ts:26](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/error-recovery/match-types.ts#L26) diff --git a/apps/docs/reference/api/generated/riviere-builder/interfaces/NearMatchQuery.md b/apps/docs/reference/api/generated/riviere-builder/interfaces/NearMatchQuery.md deleted file mode 100644 index ceb841762..000000000 --- a/apps/docs/reference/api/generated/riviere-builder/interfaces/NearMatchQuery.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: NearMatchQuery - -Defined in: [packages/riviere-builder/src/features/building/domain/error-recovery/match-types.ts:4](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/error-recovery/match-types.ts#L4) - -## Riviere-role - -value-object - -## Properties - -### domain? - -> `optional` **domain**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/error-recovery/match-types.ts:7](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/error-recovery/match-types.ts#L7) - -*** - -### name - -> **name**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/error-recovery/match-types.ts:5](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/error-recovery/match-types.ts#L5) - -*** - -### type? - -> `optional` **type**: `ComponentType` - -Defined in: [packages/riviere-builder/src/features/building/domain/error-recovery/match-types.ts:6](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/error-recovery/match-types.ts#L6) diff --git a/apps/docs/reference/api/generated/riviere-builder/interfaces/NearMatchResult.md b/apps/docs/reference/api/generated/riviere-builder/interfaces/NearMatchResult.md deleted file mode 100644 index 57975dd02..000000000 --- a/apps/docs/reference/api/generated/riviere-builder/interfaces/NearMatchResult.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: NearMatchResult - -Defined in: [packages/riviere-builder/src/features/building/domain/error-recovery/match-types.ts:18](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/error-recovery/match-types.ts#L18) - -## Riviere-role - -value-object - -## Properties - -### component - -> **component**: `Component` - -Defined in: [packages/riviere-builder/src/features/building/domain/error-recovery/match-types.ts:19](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/error-recovery/match-types.ts#L19) - -*** - -### mismatch? - -> `optional` **mismatch**: [`NearMatchMismatch`](NearMatchMismatch.md) - -Defined in: [packages/riviere-builder/src/features/building/domain/error-recovery/match-types.ts:21](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/error-recovery/match-types.ts#L21) - -*** - -### score - -> **score**: `number` - -Defined in: [packages/riviere-builder/src/features/building/domain/error-recovery/match-types.ts:20](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/error-recovery/match-types.ts#L20) diff --git a/apps/docs/reference/api/generated/riviere-builder/interfaces/RelationshipTypeInput.md b/apps/docs/reference/api/generated/riviere-builder/interfaces/RelationshipTypeInput.md deleted file mode 100644 index ba70d4e8f..000000000 --- a/apps/docs/reference/api/generated/riviere-builder/interfaces/RelationshipTypeInput.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: RelationshipTypeInput - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:115](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L115) - -## Riviere-role - -value-object - -## Properties - -### description - -> **description**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:117](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L117) - -*** - -### name - -> **name**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:116](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L116) diff --git a/apps/docs/reference/api/generated/riviere-builder/interfaces/UIInput.md b/apps/docs/reference/api/generated/riviere-builder/interfaces/UIInput.md deleted file mode 100644 index ed9ffbf07..000000000 --- a/apps/docs/reference/api/generated/riviere-builder/interfaces/UIInput.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: UIInput - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:33](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L33) - -## Riviere-role - -value-object - -## Properties - -### description? - -> `optional` **description**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:38](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L38) - -*** - -### domain - -> **domain**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:35](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L35) - -*** - -### metadata? - -> `optional` **metadata**: `Record`\<`string`, `unknown`\> - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:40](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L40) - -*** - -### module - -> **module**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:36](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L36) - -*** - -### name - -> **name**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:34](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L34) - -*** - -### route - -> **route**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:37](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L37) - -*** - -### sourceLocation - -> **sourceLocation**: `SourceLocation` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:39](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L39) diff --git a/apps/docs/reference/api/generated/riviere-builder/interfaces/UpsertOptions.md b/apps/docs/reference/api/generated/riviere-builder/interfaces/UpsertOptions.md deleted file mode 100644 index 6a6d7a74f..000000000 --- a/apps/docs/reference/api/generated/riviere-builder/interfaces/UpsertOptions.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: UpsertOptions - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:30](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L30) - -## Riviere-role - -value-object - -## Properties - -### noOverwrite? - -> `optional` **noOverwrite**: `boolean` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:30](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L30) diff --git a/apps/docs/reference/api/generated/riviere-builder/interfaces/UseCaseInput.md b/apps/docs/reference/api/generated/riviere-builder/interfaces/UseCaseInput.md deleted file mode 100644 index 3c58d75ca..000000000 --- a/apps/docs/reference/api/generated/riviere-builder/interfaces/UseCaseInput.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: UseCaseInput - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:58](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L58) - -## Riviere-role - -value-object - -## Properties - -### description? - -> `optional` **description**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:62](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L62) - -*** - -### domain - -> **domain**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:60](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L60) - -*** - -### metadata? - -> `optional` **metadata**: `Record`\<`string`, `unknown`\> - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:64](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L64) - -*** - -### module - -> **module**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:61](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L61) - -*** - -### name - -> **name**: `string` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:59](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L59) - -*** - -### sourceLocation - -> **sourceLocation**: `SourceLocation` - -Defined in: [packages/riviere-builder/src/features/building/domain/construction/construction-types.ts:63](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts#L63) diff --git a/apps/docs/reference/api/generated/riviere-query/README.md b/apps/docs/reference/api/generated/riviere-query/README.md deleted file mode 100644 index 359e2fcad..000000000 --- a/apps/docs/reference/api/generated/riviere-query/README.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -pageClass: reference ---- - -# @living-architecture/riviere-query - -## Classes - -- [ComponentNotFoundError](classes/ComponentNotFoundError.md) -- [RiviereQuery](classes/RiviereQuery.md) - -## Interfaces - -- [ComponentCounts](interfaces/ComponentCounts.md) -- [ComponentModification](interfaces/ComponentModification.md) -- [CrossDomainLink](interfaces/CrossDomainLink.md) -- [DiffStats](interfaces/DiffStats.md) -- [Domain](interfaces/Domain.md) -- [DomainConnection](interfaces/DomainConnection.md) -- [Entity](interfaces/Entity.md) -- [EntityTransition](interfaces/EntityTransition.md) -- [EventHandlerInfo](interfaces/EventHandlerInfo.md) -- [EventSubscriber](interfaces/EventSubscriber.md) -- [ExternalDomain](interfaces/ExternalDomain.md) -- [Flow](interfaces/Flow.md) -- [FlowStep](interfaces/FlowStep.md) -- [GraphDiff](interfaces/GraphDiff.md) -- [GraphStats](interfaces/GraphStats.md) -- [KnownSourceEvent](interfaces/KnownSourceEvent.md) -- [PublishedEvent](interfaces/PublishedEvent.md) -- [SearchWithFlowOptions](interfaces/SearchWithFlowOptions.md) -- [SearchWithFlowResult](interfaces/SearchWithFlowResult.md) -- [UnknownSourceEvent](interfaces/UnknownSourceEvent.md) -- [ValidationError](interfaces/ValidationError.md) -- [ValidationResult](interfaces/ValidationResult.md) - -## Type Aliases - -- [ComponentId](type-aliases/ComponentId.md) -- [DomainName](type-aliases/DomainName.md) -- [EntityName](type-aliases/EntityName.md) -- [EventId](type-aliases/EventId.md) -- [EventName](type-aliases/EventName.md) -- [HandlerId](type-aliases/HandlerId.md) -- [HandlerName](type-aliases/HandlerName.md) -- [LinkId](type-aliases/LinkId.md) -- [LinkType](type-aliases/LinkType.md) -- [OperationName](type-aliases/OperationName.md) -- [State](type-aliases/State.md) -- [SubscribedEventWithDomain](type-aliases/SubscribedEventWithDomain.md) -- [ValidationErrorCode](type-aliases/ValidationErrorCode.md) - -## Functions - -- [compareByCodePoint](functions/compareByCodePoint.md) -- [parseComponentId](functions/parseComponentId.md) -- [parseDomainName](functions/parseDomainName.md) -- [parseEntityName](functions/parseEntityName.md) -- [parseEventId](functions/parseEventId.md) -- [parseEventName](functions/parseEventName.md) -- [parseHandlerId](functions/parseHandlerId.md) -- [parseHandlerName](functions/parseHandlerName.md) -- [parseLinkId](functions/parseLinkId.md) -- [parseOperationName](functions/parseOperationName.md) -- [parseState](functions/parseState.md) diff --git a/apps/docs/reference/api/generated/riviere-query/classes/ComponentNotFoundError.md b/apps/docs/reference/api/generated/riviere-query/classes/ComponentNotFoundError.md deleted file mode 100644 index 2766dd1c7..000000000 --- a/apps/docs/reference/api/generated/riviere-query/classes/ComponentNotFoundError.md +++ /dev/null @@ -1,227 +0,0 @@ ---- -pageClass: reference ---- - -# Class: ComponentNotFoundError - -Defined in: [packages/riviere-query/src/features/querying/queries/errors.ts:2](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/errors.ts#L2) - -## Riviere-role - -query-model-error - -## Extends - -- `Error` - -## Constructors - -### Constructor - -> **new ComponentNotFoundError**(`componentId`, `suggestions`): `ComponentNotFoundError` - -Defined in: [packages/riviere-query/src/features/querying/queries/errors.ts:6](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/errors.ts#L6) - -#### Parameters - -##### componentId - -`string` - -##### suggestions - -`string`[] = `[]` - -#### Returns - -`ComponentNotFoundError` - -#### Overrides - -`Error.constructor` - -## Properties - -### cause? - -> `optional` **cause**: `unknown` - -Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/lib.es2022.error.d.ts:26 - -#### Inherited from - -`Error.cause` - -*** - -### componentId - -> `readonly` **componentId**: `string` - -Defined in: [packages/riviere-query/src/features/querying/queries/errors.ts:3](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/errors.ts#L3) - -*** - -### message - -> **message**: `string` - -Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/lib.es5.d.ts:1077 - -#### Inherited from - -`Error.message` - -*** - -### name - -> **name**: `string` - -Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/lib.es5.d.ts:1076 - -#### Inherited from - -`Error.name` - -*** - -### stack? - -> `optional` **stack**: `string` - -Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/lib.es5.d.ts:1078 - -#### Inherited from - -`Error.stack` - -*** - -### suggestions - -> `readonly` **suggestions**: `string`[] - -Defined in: [packages/riviere-query/src/features/querying/queries/errors.ts:4](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/errors.ts#L4) - -*** - -### stackTraceLimit - -> `static` **stackTraceLimit**: `number` - -Defined in: node\_modules/.pnpm/@types+node@24.10.9/node\_modules/@types/node/globals.d.ts:68 - -The `Error.stackTraceLimit` property specifies the number of stack frames -collected by a stack trace (whether generated by `new Error().stack` or -`Error.captureStackTrace(obj)`). - -The default value is `10` but may be set to any valid JavaScript number. Changes -will affect any stack trace captured _after_ the value has been changed. - -If set to a non-number value, or set to a negative number, stack traces will -not capture any frames. - -#### Inherited from - -`Error.stackTraceLimit` - -## Methods - -### captureStackTrace() - -> `static` **captureStackTrace**(`targetObject`, `constructorOpt?`): `void` - -Defined in: node\_modules/.pnpm/@types+node@24.10.9/node\_modules/@types/node/globals.d.ts:52 - -Creates a `.stack` property on `targetObject`, which when accessed returns -a string representing the location in the code at which -`Error.captureStackTrace()` was called. - -```js -const myObject = {}; -Error.captureStackTrace(myObject); -myObject.stack; // Similar to `new Error().stack` -``` - -The first line of the trace will be prefixed with -`${myObject.name}: ${myObject.message}`. - -The optional `constructorOpt` argument accepts a function. If given, all frames -above `constructorOpt`, including `constructorOpt`, will be omitted from the -generated stack trace. - -The `constructorOpt` argument is useful for hiding implementation -details of error generation from the user. For instance: - -```js -function a() { - b(); -} - -function b() { - c(); -} - -function c() { - // Create an error without stack trace to avoid calculating the stack trace twice. - const { stackTraceLimit } = Error; - Error.stackTraceLimit = 0; - const error = new Error(); - Error.stackTraceLimit = stackTraceLimit; - - // Capture the stack trace above function b - Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace - throw error; -} - -a(); -``` - -#### Parameters - -##### targetObject - -`object` - -##### constructorOpt? - -`Function` - -#### Returns - -`void` - -#### Inherited from - -`Error.captureStackTrace` - -*** - -### prepareStackTrace() - -> `static` **prepareStackTrace**(`err`, `stackTraces`): `any` - -Defined in: node\_modules/.pnpm/@types+node@24.10.9/node\_modules/@types/node/globals.d.ts:56 - -#### Parameters - -##### err - -`Error` - -##### stackTraces - -`CallSite`[] - -#### Returns - -`any` - -#### See - -https://v8.dev/docs/stack-trace-api#customizing-stack-traces - -#### Inherited from - -`Error.prepareStackTrace` diff --git a/apps/docs/reference/api/generated/riviere-query/classes/RiviereQuery.md b/apps/docs/reference/api/generated/riviere-query/classes/RiviereQuery.md deleted file mode 100644 index eed26919f..000000000 --- a/apps/docs/reference/api/generated/riviere-query/classes/RiviereQuery.md +++ /dev/null @@ -1,980 +0,0 @@ ---- -pageClass: reference ---- - -# Class: RiviereQuery - -Defined in: [packages/riviere-query/src/features/querying/queries/RiviereQuery.ts:119](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/RiviereQuery.ts#L119) - -Query and analyze Riviere architecture graphs. - -RiviereQuery provides methods to explore components, trace execution flows, -analyze domain models, and compare graph versions. - -## Example - -```typescript -import { RiviereQuery } from '@living-architecture/riviere-query' - -// From JSON -const query = RiviereQuery.fromJSON(graphData) - -// Query components -const apis = query.componentsByType('API') -const orderDomain = query.componentsInDomain('orders') - -// Trace flows -const flow = query.traceFlow('orders:checkout:api:post-orders') -``` - -## Riviere-role - -query-model - -## Constructors - -### Constructor - -> **new RiviereQuery**(`graph`): `RiviereQuery` - -Defined in: [packages/riviere-query/src/features/querying/queries/RiviereQuery.ts:134](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/RiviereQuery.ts#L134) - -Creates a new RiviereQuery instance. - -#### Parameters - -##### graph - -`RiviereGraph` - -A valid RiviereGraph object - -#### Returns - -`RiviereQuery` - -#### Throws - -If the graph fails schema validation - -#### Example - -```typescript -const graph: RiviereGraph = JSON.parse(jsonString) -const query = new RiviereQuery(graph) -``` - -## Methods - -### businessRulesFor() - -> **businessRulesFor**(`entityName`): `string`[] - -Defined in: [packages/riviere-query/src/features/querying/queries/RiviereQuery.ts:382](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/RiviereQuery.ts#L382) - -Returns all business rules for an entity's operations. - -#### Parameters - -##### entityName - -`string` - -The entity name to get rules for - -#### Returns - -`string`[] - -Array of business rule strings - -#### Example - -```typescript -const rules = query.businessRulesFor('Order') -``` - -*** - -### componentById() - -> **componentById**(`id`): `Component` \| `undefined` - -Defined in: [packages/riviere-query/src/features/querying/queries/RiviereQuery.ts:266](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/RiviereQuery.ts#L266) - -Finds a component by its ID. - -#### Parameters - -##### id - -`string` & `$brand`\<`"ComponentId"`\> - -The component ID to look up - -#### Returns - -`Component` \| `undefined` - -The component, or undefined if not found - -#### Example - -```typescript -const component = query.componentById('orders:checkout:api:post-orders') -``` - -*** - -### components() - -> **components**(): `Component`[] - -Defined in: [packages/riviere-query/src/features/querying/queries/RiviereQuery.ts:168](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/RiviereQuery.ts#L168) - -Returns all components in the graph. - -#### Returns - -`Component`[] - -Array of all components - -#### Example - -```typescript -const allComponents = query.components() -console.log(`Total: ${allComponents.length}`) -``` - -*** - -### componentsByType() - -> **componentsByType**(`type`): `Component`[] - -Defined in: [packages/riviere-query/src/features/querying/queries/RiviereQuery.ts:315](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/RiviereQuery.ts#L315) - -Returns all components of a specific type. - -#### Parameters - -##### type - -`ComponentType` - -The component type to filter by - -#### Returns - -`Component`[] - -Array of components of that type - -#### Example - -```typescript -const apis = query.componentsByType('API') -const events = query.componentsByType('Event') -``` - -*** - -### componentsInDomain() - -> **componentsInDomain**(`domainName`): `Component`[] - -Defined in: [packages/riviere-query/src/features/querying/queries/RiviereQuery.ts:299](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/RiviereQuery.ts#L299) - -Returns all components in a specific domain. - -#### Parameters - -##### domainName - -`string` - -The domain name to filter by - -#### Returns - -`Component`[] - -Array of components in the domain - -#### Example - -```typescript -const orderComponents = query.componentsInDomain('orders') -``` - -*** - -### crossDomainLinks() - -> **crossDomainLinks**(`domainName`): [`CrossDomainLink`](../interfaces/CrossDomainLink.md)[] - -Defined in: [packages/riviere-query/src/features/querying/queries/RiviereQuery.ts:569](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/RiviereQuery.ts#L569) - -Returns links from a domain to other domains. - -#### Parameters - -##### domainName - -`string` - -The source domain name - -#### Returns - -[`CrossDomainLink`](../interfaces/CrossDomainLink.md)[] - -Array of CrossDomainLink objects (deduplicated by target domain and type) - -#### Example - -```typescript -const outgoing = query.crossDomainLinks('orders') -``` - -*** - -### detectOrphans() - -> **detectOrphans**(): `string` & `$brand`\<`"ComponentId"`\>[] - -Defined in: [packages/riviere-query/src/features/querying/queries/RiviereQuery.ts:219](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/RiviereQuery.ts#L219) - -Detects orphan components with no incoming or outgoing links. - -#### Returns - -`string` & `$brand`\<`"ComponentId"`\>[] - -Array of component IDs that are disconnected from the graph - -#### Example - -```typescript -const orphanIds = query.detectOrphans() -if (orphanIds.length > 0) { - console.warn(`Found ${orphanIds.length} orphan nodes`) -} -``` - -*** - -### diff() - -> **diff**(`other`): [`GraphDiff`](../interfaces/GraphDiff.md) - -Defined in: [packages/riviere-query/src/features/querying/queries/RiviereQuery.ts:474](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/RiviereQuery.ts#L474) - -Compares this graph with another and returns the differences. - -#### Parameters - -##### other - -`RiviereGraph` - -The graph to compare against - -#### Returns - -[`GraphDiff`](../interfaces/GraphDiff.md) - -GraphDiff with added, removed, and modified items - -#### Example - -```typescript -const oldGraph = RiviereQuery.fromJSON(oldData) -const newGraph = RiviereQuery.fromJSON(newData) -const diff = newGraph.diff(oldGraph.graph) - -console.log(`Added: ${diff.stats.componentsAdded}`) -console.log(`Removed: ${diff.stats.componentsRemoved}`) -``` - -*** - -### domainConnections() - -> **domainConnections**(`domainName`): [`DomainConnection`](../interfaces/DomainConnection.md)[] - -Defined in: [packages/riviere-query/src/features/querying/queries/RiviereQuery.ts:589](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/RiviereQuery.ts#L589) - -Returns cross-domain connections with API and event counts. - -Shows both incoming and outgoing connections for a domain. - -#### Parameters - -##### domainName - -`string` - -The domain to analyze - -#### Returns - -[`DomainConnection`](../interfaces/DomainConnection.md)[] - -Array of DomainConnection objects - -#### Example - -```typescript -const connections = query.domainConnections('orders') -for (const conn of connections) { - console.log(`${conn.direction} to ${conn.targetDomain}: ${conn.apiCount} API, ${conn.eventCount} event`) -} -``` - -*** - -### domains() - -> **domains**(): [`Domain`](../interfaces/Domain.md)[] - -Defined in: [packages/riviere-query/src/features/querying/queries/RiviereQuery.ts:332](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/RiviereQuery.ts#L332) - -Returns domain information with component counts. - -#### Returns - -[`Domain`](../interfaces/Domain.md)[] - -Array of Domain objects sorted by name - -#### Example - -```typescript -const domains = query.domains() -for (const domain of domains) { - console.log(`${domain.name}: ${domain.componentCounts.total} components`) -} -``` - -*** - -### entities() - -> **entities**(`domainName?`): [`Entity`](../interfaces/Entity.md)[] - -Defined in: [packages/riviere-query/src/features/querying/queries/RiviereQuery.ts:367](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/RiviereQuery.ts#L367) - -Returns entities with their domain operations. - -#### Parameters - -##### domainName? - -`string` - -Optional domain to filter by - -#### Returns - -[`Entity`](../interfaces/Entity.md)[] - -Array of Entity objects with their operations - -#### Example - -```typescript -const allEntities = query.entities() -const orderEntities = query.entities('orders') - -for (const entity of orderEntities) { - console.log(`${entity.name} has ${entity.operations.length} operations`) -} -``` - -*** - -### entryPoints() - -> **entryPoints**(): `Component`[] - -Defined in: [packages/riviere-query/src/features/querying/queries/RiviereQuery.ts:432](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/RiviereQuery.ts#L432) - -Returns components that are entry points to the system. - -Entry points are UI, API, EventHandler, or Custom components -with no incoming links. - -#### Returns - -`Component`[] - -Array of entry point components - -#### Example - -```typescript -const entryPoints = query.entryPoints() -``` - -*** - -### eventHandlers() - -> **eventHandlers**(`eventName?`): [`EventHandlerInfo`](../interfaces/EventHandlerInfo.md)[] - -Defined in: [packages/riviere-query/src/features/querying/queries/RiviereQuery.ts:510](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/RiviereQuery.ts#L510) - -Returns event handlers with their subscriptions. - -#### Parameters - -##### eventName? - -`string` - -Optional event name to filter handlers by - -#### Returns - -[`EventHandlerInfo`](../interfaces/EventHandlerInfo.md)[] - -Array of EventHandlerInfo objects sorted by handler name - -#### Example - -```typescript -const allHandlers = query.eventHandlers() -const orderPlacedHandlers = query.eventHandlers('order-placed') -``` - -*** - -### externalDomains() - -> **externalDomains**(): [`ExternalDomain`](../interfaces/ExternalDomain.md)[] - -Defined in: [packages/riviere-query/src/features/querying/queries/RiviereQuery.ts:665](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/RiviereQuery.ts#L665) - -Returns external domains that components connect to. - -Each unique external target is returned as a separate ExternalDomain, -with aggregated source domains and connection counts. - -#### Returns - -[`ExternalDomain`](../interfaces/ExternalDomain.md)[] - -Array of ExternalDomain objects, sorted alphabetically by name - -#### Example - -```typescript -const externals = query.externalDomains() -for (const ext of externals) { - console.log(`${ext.name}: ${ext.connectionCount} connections from ${ext.sourceDomains.join(', ')}`) -} -``` - -*** - -### externalLinks() - -> **externalLinks**(): `ExternalLink`[] - -Defined in: [packages/riviere-query/src/features/querying/queries/RiviereQuery.ts:645](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/RiviereQuery.ts#L645) - -Returns all external links in the graph. - -External links represent connections from components to external -systems that are not part of the graph (e.g., third-party APIs). - -#### Returns - -`ExternalLink`[] - -Array of all external links, or empty array if none exist - -#### Example - -```typescript -const externalLinks = query.externalLinks() -for (const link of externalLinks) { - console.log(`${link.source} -> ${link.target.name}`) -} -``` - -*** - -### find() - -> **find**(`predicate`): `Component` \| `undefined` - -Defined in: [packages/riviere-query/src/features/querying/queries/RiviereQuery.ts:234](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/RiviereQuery.ts#L234) - -Finds the first component matching a predicate. - -#### Parameters - -##### predicate - -(`component`) => `boolean` - -Function that returns true for matching components - -#### Returns - -`Component` \| `undefined` - -The first matching component, or undefined if none found - -#### Example - -```typescript -const checkout = query.find(c => c.name.includes('checkout')) -``` - -*** - -### findAll() - -> **findAll**(`predicate`): `Component`[] - -Defined in: [packages/riviere-query/src/features/querying/queries/RiviereQuery.ts:251](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/RiviereQuery.ts#L251) - -Finds all components matching a predicate. - -#### Parameters - -##### predicate - -(`component`) => `boolean` - -Function that returns true for matching components - -#### Returns - -`Component`[] - -Array of all matching components - -#### Example - -```typescript -const orderHandlers = query.findAll(c => - c.type === 'EventHandler' && c.domain === 'orders' -) -``` - -*** - -### flows() - -> **flows**(): [`Flow`](../interfaces/Flow.md)[] - -Defined in: [packages/riviere-query/src/features/querying/queries/RiviereQuery.ts:534](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/RiviereQuery.ts#L534) - -Returns all flows in the graph. - -Each flow starts from an entry point (UI, API, or Custom with no -incoming links) and traces forward through the graph. - -#### Returns - -[`Flow`](../interfaces/Flow.md)[] - -Array of Flow objects with entry point and steps - -#### Example - -```typescript -const flows = query.flows() - -for (const flow of flows) { - console.log(`Flow: ${flow.entryPoint.name}`) - for (const step of flow.steps) { - console.log(` ${step.component.name} (depth: ${step.depth})`) - } -} -``` - -*** - -### links() - -> **links**(): `Link`[] - -Defined in: [packages/riviere-query/src/features/querying/queries/RiviereQuery.ts:183](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/RiviereQuery.ts#L183) - -Returns all links in the graph. - -#### Returns - -`Link`[] - -Array of all links - -#### Example - -```typescript -const allLinks = query.links() -console.log(`Total links: ${allLinks.length}`) -``` - -*** - -### nodeDepths() - -> **nodeDepths**(): `Map`\<`string` & `$brand`\<`"ComponentId"`\>, `number`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/RiviereQuery.ts:625](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/RiviereQuery.ts#L625) - -Calculates depth from entry points for each component. - -Components unreachable from entry points will not be in the map. - -#### Returns - -`Map`\<`string` & `$brand`\<`"ComponentId"`\>, `number`\> - -Map of component ID to depth (0 = entry point) - -#### Example - -```typescript -const depths = query.nodeDepths() -for (const [id, depth] of depths) { - console.log(`${id}: depth ${depth}`) -} -``` - -*** - -### operationsFor() - -> **operationsFor**(`entityName`): `DomainOpComponent`[] - -Defined in: [packages/riviere-query/src/features/querying/queries/RiviereQuery.ts:347](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/RiviereQuery.ts#L347) - -Returns all domain operations for a specific entity. - -#### Parameters - -##### entityName - -`string` - -The entity name to get operations for - -#### Returns - -`DomainOpComponent`[] - -Array of DomainOp components targeting the entity - -#### Example - -```typescript -const orderOps = query.operationsFor('Order') -``` - -*** - -### publishedEvents() - -> **publishedEvents**(`domainName?`): [`PublishedEvent`](../interfaces/PublishedEvent.md)[] - -Defined in: [packages/riviere-query/src/features/querying/queries/RiviereQuery.ts:494](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/RiviereQuery.ts#L494) - -Returns published events with their handlers. - -#### Parameters - -##### domainName? - -`string` - -Optional domain to filter by - -#### Returns - -[`PublishedEvent`](../interfaces/PublishedEvent.md)[] - -Array of PublishedEvent objects sorted by event name - -#### Example - -```typescript -const allEvents = query.publishedEvents() -const orderEvents = query.publishedEvents('orders') - -for (const event of orderEvents) { - console.log(`${event.eventName} has ${event.handlers.length} handlers`) -} -``` - -*** - -### search() - -> **search**(`query`): `Component`[] - -Defined in: [packages/riviere-query/src/features/querying/queries/RiviereQuery.ts:284](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/RiviereQuery.ts#L284) - -Searches components by name, domain, or type. - -Case-insensitive search across component name, domain, and type fields. - -#### Parameters - -##### query - -`string` - -Search term - -#### Returns - -`Component`[] - -Array of matching components - -#### Example - -```typescript -const results = query.search('order') -// Matches: "PlaceOrder", "orders" domain, etc. -``` - -*** - -### searchWithFlow() - -> **searchWithFlow**(`query`, `options`): [`SearchWithFlowResult`](../interfaces/SearchWithFlowResult.md) - -Defined in: [packages/riviere-query/src/features/querying/queries/RiviereQuery.ts:554](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/RiviereQuery.ts#L554) - -Searches for components and returns their flow context. - -Returns both matching component IDs and all visible IDs in their flows. - -#### Parameters - -##### query - -`string` - -Search term - -##### options - -[`SearchWithFlowOptions`](../interfaces/SearchWithFlowOptions.md) - -Search options including returnAllOnEmptyQuery - -#### Returns - -[`SearchWithFlowResult`](../interfaces/SearchWithFlowResult.md) - -Object with matchingIds and visibleIds arrays - -#### Example - -```typescript -const result = query.searchWithFlow('checkout', { returnAllOnEmptyQuery: true }) -console.log(`Found ${result.matchingIds.length} matches`) -console.log(`Showing ${result.visibleIds.length} nodes in context`) -``` - -*** - -### statesFor() - -> **statesFor**(`entityName`): `string` & `$brand`\<`"State"`\>[] - -Defined in: [packages/riviere-query/src/features/querying/queries/RiviereQuery.ts:415](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/RiviereQuery.ts#L415) - -Returns ordered states for an entity based on transitions. - -States are ordered by transition flow from initial to final states. - -#### Parameters - -##### entityName - -`string` - -The entity name to get states for - -#### Returns - -`string` & `$brand`\<`"State"`\>[] - -Array of state names in transition order - -#### Example - -```typescript -const orderStates = query.statesFor('Order') -// ['pending', 'confirmed', 'shipped', 'delivered'] -``` - -*** - -### stats() - -> **stats**(): [`GraphStats`](../interfaces/GraphStats.md) - -Defined in: [packages/riviere-query/src/features/querying/queries/RiviereQuery.ts:606](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/RiviereQuery.ts#L606) - -Returns aggregate statistics about the graph. - -#### Returns - -[`GraphStats`](../interfaces/GraphStats.md) - -GraphStats with counts for components, links, domains, APIs, entities, and events - -#### Example - -```typescript -const stats = query.stats() -console.log(`Components: ${stats.componentCount}`) -console.log(`Links: ${stats.linkCount}`) -console.log(`Domains: ${stats.domainCount}`) -``` - -*** - -### traceFlow() - -> **traceFlow**(`startComponentId`): `object` - -Defined in: [packages/riviere-query/src/features/querying/queries/RiviereQuery.ts:451](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/RiviereQuery.ts#L451) - -Traces the complete flow bidirectionally from a starting component. - -Returns all nodes and links connected to the starting point, -following links in both directions. - -#### Parameters - -##### startComponentId - -`string` & `$brand`\<`"ComponentId"`\> - -ID of the component to start tracing from - -#### Returns - -`object` - -Object with componentIds and linkIds in the flow - -##### componentIds - -> **componentIds**: `string` & `$brand`\<`"ComponentId"`\>[] - -##### linkIds - -> **linkIds**: `string` & `$brand`\<`"LinkId"`\>[] - -#### Example - -```typescript -const flow = query.traceFlow('orders:checkout:api:post-orders') -console.log(`Flow includes ${flow.componentIds.length} nodes`) -``` - -*** - -### transitionsFor() - -> **transitionsFor**(`entityName`): [`EntityTransition`](../interfaces/EntityTransition.md)[] - -Defined in: [packages/riviere-query/src/features/querying/queries/RiviereQuery.ts:397](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/RiviereQuery.ts#L397) - -Returns state transitions for an entity. - -#### Parameters - -##### entityName - -`string` - -The entity name to get transitions for - -#### Returns - -[`EntityTransition`](../interfaces/EntityTransition.md)[] - -Array of EntityTransition objects - -#### Example - -```typescript -const transitions = query.transitionsFor('Order') -``` - -*** - -### validate() - -> **validate**(): [`ValidationResult`](../interfaces/ValidationResult.md) - -Defined in: [packages/riviere-query/src/features/querying/queries/RiviereQuery.ts:202](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/RiviereQuery.ts#L202) - -Validates the graph structure beyond schema validation. - -Checks for structural issues like invalid link references. - -#### Returns - -[`ValidationResult`](../interfaces/ValidationResult.md) - -Validation result with any errors found - -#### Example - -```typescript -const result = query.validate() -if (!result.valid) { - console.error('Validation errors:', result.errors) -} -``` - -*** - -### fromJSON() - -> `static` **fromJSON**(`json`): `RiviereQuery` - -Defined in: [packages/riviere-query/src/features/querying/queries/RiviereQuery.ts:152](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/RiviereQuery.ts#L152) - -Creates a RiviereQuery from raw JSON data. - -#### Parameters - -##### json - -`unknown` - -Raw JSON data to parse as a RiviereGraph - -#### Returns - -`RiviereQuery` - -A new RiviereQuery instance - -#### Throws - -If the JSON fails schema validation - -#### Example - -```typescript -const jsonData = await fetch('/graph.json').then(r => r.json()) -const query = RiviereQuery.fromJSON(jsonData) -``` diff --git a/apps/docs/reference/api/generated/riviere-query/functions/compareByCodePoint.md b/apps/docs/reference/api/generated/riviere-query/functions/compareByCodePoint.md deleted file mode 100644 index d6c8cf3ef..000000000 --- a/apps/docs/reference/api/generated/riviere-query/functions/compareByCodePoint.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -pageClass: reference ---- - -# Function: compareByCodePoint() - -> **compareByCodePoint**(`a`, `b`): `number` - -Defined in: [packages/riviere-query/src/features/querying/queries/compare-by-code-point.ts:2](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/compare-by-code-point.ts#L2) - -## Parameters - -### a - -`string` - -### b - -`string` - -## Returns - -`number` - -## Riviere-role - -query-model diff --git a/apps/docs/reference/api/generated/riviere-query/functions/parseComponentId.md b/apps/docs/reference/api/generated/riviere-query/functions/parseComponentId.md deleted file mode 100644 index ec83b509a..000000000 --- a/apps/docs/reference/api/generated/riviere-query/functions/parseComponentId.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -pageClass: reference ---- - -# Function: parseComponentId() - -> **parseComponentId**(`id`): `string` & `$brand`\<`"ComponentId"`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:329](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L329) - -Parses a string as a ComponentId. - -## Parameters - -### id - -`string` - -The string to parse - -## Returns - -`string` & `$brand`\<`"ComponentId"`\> - -A branded ComponentId - -## Riviere-role - -query-model diff --git a/apps/docs/reference/api/generated/riviere-query/functions/parseDomainName.md b/apps/docs/reference/api/generated/riviere-query/functions/parseDomainName.md deleted file mode 100644 index cf58c7d0b..000000000 --- a/apps/docs/reference/api/generated/riviere-query/functions/parseDomainName.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -pageClass: reference ---- - -# Function: parseDomainName() - -> **parseDomainName**(`value`): `string` & `$brand`\<`"DomainName"`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:362](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L362) - -Parses a string as a DomainName. - -## Parameters - -### value - -`string` - -The string to parse - -## Returns - -`string` & `$brand`\<`"DomainName"`\> - -A branded DomainName - -## Riviere-role - -query-model diff --git a/apps/docs/reference/api/generated/riviere-query/functions/parseEntityName.md b/apps/docs/reference/api/generated/riviere-query/functions/parseEntityName.md deleted file mode 100644 index 0a47ea5c6..000000000 --- a/apps/docs/reference/api/generated/riviere-query/functions/parseEntityName.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -pageClass: reference ---- - -# Function: parseEntityName() - -> **parseEntityName**(`value`): `string` & `$brand`\<`"EntityName"`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:351](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L351) - -Parses a string as an EntityName. - -## Parameters - -### value - -`string` - -The string to parse - -## Returns - -`string` & `$brand`\<`"EntityName"`\> - -A branded EntityName - -## Riviere-role - -query-model diff --git a/apps/docs/reference/api/generated/riviere-query/functions/parseEventId.md b/apps/docs/reference/api/generated/riviere-query/functions/parseEventId.md deleted file mode 100644 index 15dcdee14..000000000 --- a/apps/docs/reference/api/generated/riviere-query/functions/parseEventId.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -pageClass: reference ---- - -# Function: parseEventId() - -> **parseEventId**(`value`): `string` & `$brand`\<`"EventId"`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:395](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L395) - -Parses a string as an EventId. - -## Parameters - -### value - -`string` - -The string to parse - -## Returns - -`string` & `$brand`\<`"EventId"`\> - -A branded EventId - -## Riviere-role - -query-model diff --git a/apps/docs/reference/api/generated/riviere-query/functions/parseEventName.md b/apps/docs/reference/api/generated/riviere-query/functions/parseEventName.md deleted file mode 100644 index 931b5fbbf..000000000 --- a/apps/docs/reference/api/generated/riviere-query/functions/parseEventName.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -pageClass: reference ---- - -# Function: parseEventName() - -> **parseEventName**(`value`): `string` & `$brand`\<`"EventName"`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:406](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L406) - -Parses a string as an EventName. - -## Parameters - -### value - -`string` - -The string to parse - -## Returns - -`string` & `$brand`\<`"EventName"`\> - -A branded EventName - -## Riviere-role - -query-model diff --git a/apps/docs/reference/api/generated/riviere-query/functions/parseHandlerId.md b/apps/docs/reference/api/generated/riviere-query/functions/parseHandlerId.md deleted file mode 100644 index 2eb295f54..000000000 --- a/apps/docs/reference/api/generated/riviere-query/functions/parseHandlerId.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -pageClass: reference ---- - -# Function: parseHandlerId() - -> **parseHandlerId**(`value`): `string` & `$brand`\<`"HandlerId"`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:417](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L417) - -Parses a string as a HandlerId. - -## Parameters - -### value - -`string` - -The string to parse - -## Returns - -`string` & `$brand`\<`"HandlerId"`\> - -A branded HandlerId - -## Riviere-role - -query-model diff --git a/apps/docs/reference/api/generated/riviere-query/functions/parseHandlerName.md b/apps/docs/reference/api/generated/riviere-query/functions/parseHandlerName.md deleted file mode 100644 index 650757679..000000000 --- a/apps/docs/reference/api/generated/riviere-query/functions/parseHandlerName.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -pageClass: reference ---- - -# Function: parseHandlerName() - -> **parseHandlerName**(`value`): `string` & `$brand`\<`"HandlerName"`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:428](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L428) - -Parses a string as a HandlerName. - -## Parameters - -### value - -`string` - -The string to parse - -## Returns - -`string` & `$brand`\<`"HandlerName"`\> - -A branded HandlerName - -## Riviere-role - -query-model diff --git a/apps/docs/reference/api/generated/riviere-query/functions/parseLinkId.md b/apps/docs/reference/api/generated/riviere-query/functions/parseLinkId.md deleted file mode 100644 index 47b7c85f3..000000000 --- a/apps/docs/reference/api/generated/riviere-query/functions/parseLinkId.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -pageClass: reference ---- - -# Function: parseLinkId() - -> **parseLinkId**(`id`): `string` & `$brand`\<`"LinkId"`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:340](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L340) - -Parses a string as a LinkId. - -## Parameters - -### id - -`string` - -The string to parse - -## Returns - -`string` & `$brand`\<`"LinkId"`\> - -A branded LinkId - -## Riviere-role - -query-model diff --git a/apps/docs/reference/api/generated/riviere-query/functions/parseOperationName.md b/apps/docs/reference/api/generated/riviere-query/functions/parseOperationName.md deleted file mode 100644 index 4ffe7047d..000000000 --- a/apps/docs/reference/api/generated/riviere-query/functions/parseOperationName.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -pageClass: reference ---- - -# Function: parseOperationName() - -> **parseOperationName**(`value`): `string` & `$brand`\<`"OperationName"`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:384](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L384) - -Parses a string as an OperationName. - -## Parameters - -### value - -`string` - -The string to parse - -## Returns - -`string` & `$brand`\<`"OperationName"`\> - -A branded OperationName - -## Riviere-role - -query-model diff --git a/apps/docs/reference/api/generated/riviere-query/functions/parseState.md b/apps/docs/reference/api/generated/riviere-query/functions/parseState.md deleted file mode 100644 index f8aaf1e63..000000000 --- a/apps/docs/reference/api/generated/riviere-query/functions/parseState.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -pageClass: reference ---- - -# Function: parseState() - -> **parseState**(`value`): `string` & `$brand`\<`"State"`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:373](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L373) - -Parses a string as a State. - -## Parameters - -### value - -`string` - -The string to parse - -## Returns - -`string` & `$brand`\<`"State"`\> - -A branded State - -## Riviere-role - -query-model diff --git a/apps/docs/reference/api/generated/riviere-query/interfaces/ComponentCounts.md b/apps/docs/reference/api/generated/riviere-query/interfaces/ComponentCounts.md deleted file mode 100644 index b09679b50..000000000 --- a/apps/docs/reference/api/generated/riviere-query/interfaces/ComponentCounts.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: ComponentCounts - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:127](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L127) - -Component counts by type within a domain. - -## Riviere-role - -query-model - -## Properties - -### API - -> **API**: `number` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:131](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L131) - -Number of API components. - -*** - -### Custom - -> **Custom**: `number` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:141](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L141) - -Number of Custom components. - -*** - -### DomainOp - -> **DomainOp**: `number` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:135](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L135) - -Number of DomainOp components. - -*** - -### Event - -> **Event**: `number` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:137](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L137) - -Number of Event components. - -*** - -### EventHandler - -> **EventHandler**: `number` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:139](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L139) - -Number of EventHandler components. - -*** - -### total - -> **total**: `number` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:143](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L143) - -Total number of components. - -*** - -### UI - -> **UI**: `number` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:129](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L129) - -Number of UI components. - -*** - -### UseCase - -> **UseCase**: `number` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:133](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L133) - -Number of UseCase components. diff --git a/apps/docs/reference/api/generated/riviere-query/interfaces/ComponentModification.md b/apps/docs/reference/api/generated/riviere-query/interfaces/ComponentModification.md deleted file mode 100644 index a180be13a..000000000 --- a/apps/docs/reference/api/generated/riviere-query/interfaces/ComponentModification.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: ComponentModification - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:165](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L165) - -A component that was modified between graph versions. - -## Riviere-role - -query-model - -## Properties - -### after - -> **after**: `Component` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:171](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L171) - -The component state after modification. - -*** - -### before - -> **before**: `Component` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:169](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L169) - -The component state before modification. - -*** - -### changedFields - -> **changedFields**: `string`[] - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:173](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L173) - -List of field names that changed. - -*** - -### id - -> **id**: `string` & `$brand`\<`"ComponentId"`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:167](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L167) - -The component ID. diff --git a/apps/docs/reference/api/generated/riviere-query/interfaces/CrossDomainLink.md b/apps/docs/reference/api/generated/riviere-query/interfaces/CrossDomainLink.md deleted file mode 100644 index 7f10cbe4a..000000000 --- a/apps/docs/reference/api/generated/riviere-query/interfaces/CrossDomainLink.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: CrossDomainLink - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:265](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L265) - -A link that crosses domain boundaries. - -## Riviere-role - -query-model - -## Properties - -### linkType - -> **linkType**: [`LinkType`](../type-aliases/LinkType.md) \| `undefined` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:269](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L269) - -Type of the cross-domain link. - -*** - -### targetDomain - -> **targetDomain**: `string` & `$brand`\<`"DomainName"`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:267](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L267) - -The target domain name. diff --git a/apps/docs/reference/api/generated/riviere-query/interfaces/DiffStats.md b/apps/docs/reference/api/generated/riviere-query/interfaces/DiffStats.md deleted file mode 100644 index e3f45a23c..000000000 --- a/apps/docs/reference/api/generated/riviere-query/interfaces/DiffStats.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: DiffStats - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:180](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L180) - -Summary statistics of differences between graphs. - -## Riviere-role - -query-model - -## Properties - -### componentsAdded - -> **componentsAdded**: `number` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:182](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L182) - -Number of components added. - -*** - -### componentsModified - -> **componentsModified**: `number` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:186](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L186) - -Number of components modified. - -*** - -### componentsRemoved - -> **componentsRemoved**: `number` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:184](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L184) - -Number of components removed. - -*** - -### linksAdded - -> **linksAdded**: `number` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:188](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L188) - -Number of links added. - -*** - -### linksRemoved - -> **linksRemoved**: `number` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:190](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L190) - -Number of links removed. diff --git a/apps/docs/reference/api/generated/riviere-query/interfaces/Domain.md b/apps/docs/reference/api/generated/riviere-query/interfaces/Domain.md deleted file mode 100644 index 164df9368..000000000 --- a/apps/docs/reference/api/generated/riviere-query/interfaces/Domain.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: Domain - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:150](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L150) - -Domain information with metadata and component counts. - -## Riviere-role - -query-model - -## Properties - -### componentCounts - -> **componentCounts**: [`ComponentCounts`](ComponentCounts.md) - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:158](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L158) - -Counts of components by type. - -*** - -### description - -> **description**: `string` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:154](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L154) - -Domain description from graph metadata. - -*** - -### name - -> **name**: `string` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:152](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L152) - -Domain name. - -*** - -### systemType - -> **systemType**: `SystemType` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:156](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L156) - -System type classification. diff --git a/apps/docs/reference/api/generated/riviere-query/interfaces/DomainConnection.md b/apps/docs/reference/api/generated/riviere-query/interfaces/DomainConnection.md deleted file mode 100644 index 34c422843..000000000 --- a/apps/docs/reference/api/generated/riviere-query/interfaces/DomainConnection.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: DomainConnection - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:276](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L276) - -Summary of connections between domains. - -## Riviere-role - -query-model - -## Properties - -### apiCount - -> **apiCount**: `number` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:282](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L282) - -Number of API-based connections. - -*** - -### direction - -> **direction**: `"outgoing"` \| `"incoming"` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:280](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L280) - -Direction relative to the queried domain. - -*** - -### eventCount - -> **eventCount**: `number` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:284](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L284) - -Number of event-based connections. - -*** - -### targetDomain - -> **targetDomain**: `string` & `$brand`\<`"DomainName"`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:278](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L278) - -The connected domain name. diff --git a/apps/docs/reference/api/generated/riviere-query/interfaces/Entity.md b/apps/docs/reference/api/generated/riviere-query/interfaces/Entity.md deleted file mode 100644 index 7590717f8..000000000 --- a/apps/docs/reference/api/generated/riviere-query/interfaces/Entity.md +++ /dev/null @@ -1,109 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: Entity - -Defined in: [packages/riviere-query/src/features/querying/queries/event-types.ts:17](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/event-types.ts#L17) - -A domain entity with its associated operations, states, and business rules. - -## Riviere-role - -query-model - -## Properties - -### businessRules - -> `readonly` **businessRules**: `string`[] - -Defined in: [packages/riviere-query/src/features/querying/queries/event-types.ts:30](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/event-types.ts#L30) - -Deduplicated business rules from all operations. - -*** - -### domain - -> `readonly` **domain**: `string` & `$brand`\<`"DomainName"`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/event-types.ts:22](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/event-types.ts#L22) - -The domain containing the entity. - -*** - -### name - -> `readonly` **name**: `string` & `$brand`\<`"EntityName"`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/event-types.ts:20](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/event-types.ts#L20) - -The entity name. - -*** - -### operations - -> `readonly` **operations**: `DomainOpComponent`[] - -Defined in: [packages/riviere-query/src/features/querying/queries/event-types.ts:24](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/event-types.ts#L24) - -All domain operations targeting this entity. - -*** - -### states - -> `readonly` **states**: `string` & `$brand`\<`"State"`\>[] - -Defined in: [packages/riviere-query/src/features/querying/queries/event-types.ts:26](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/event-types.ts#L26) - -Ordered states derived from state transitions (initial → terminal). - -*** - -### transitions - -> `readonly` **transitions**: [`EntityTransition`](EntityTransition.md)[] - -Defined in: [packages/riviere-query/src/features/querying/queries/event-types.ts:28](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/event-types.ts#L28) - -State transitions with triggering operations. - -## Methods - -### firstOperationId() - -> **firstOperationId**(): `string` \| `undefined` - -Defined in: [packages/riviere-query/src/features/querying/queries/event-types.ts:41](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/event-types.ts#L41) - -#### Returns - -`string` \| `undefined` - -*** - -### hasBusinessRules() - -> **hasBusinessRules**(): `boolean` - -Defined in: [packages/riviere-query/src/features/querying/queries/event-types.ts:37](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/event-types.ts#L37) - -#### Returns - -`boolean` - -*** - -### hasStates() - -> **hasStates**(): `boolean` - -Defined in: [packages/riviere-query/src/features/querying/queries/event-types.ts:33](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/event-types.ts#L33) - -#### Returns - -`boolean` diff --git a/apps/docs/reference/api/generated/riviere-query/interfaces/EntityTransition.md b/apps/docs/reference/api/generated/riviere-query/interfaces/EntityTransition.md deleted file mode 100644 index fff92a17c..000000000 --- a/apps/docs/reference/api/generated/riviere-query/interfaces/EntityTransition.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: EntityTransition - -Defined in: [packages/riviere-query/src/features/querying/queries/event-types.ts:50](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/event-types.ts#L50) - -A state transition in an entity's state machine. - -## Riviere-role - -query-model - -## Properties - -### from - -> **from**: `string` & `$brand`\<`"State"`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/event-types.ts:52](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/event-types.ts#L52) - -The state before the transition. - -*** - -### to - -> **to**: `string` & `$brand`\<`"State"`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/event-types.ts:54](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/event-types.ts#L54) - -The state after the transition. - -*** - -### triggeredBy - -> **triggeredBy**: `string` & `$brand`\<`"OperationName"`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/event-types.ts:56](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/event-types.ts#L56) - -The operation that triggers this transition. diff --git a/apps/docs/reference/api/generated/riviere-query/interfaces/EventHandlerInfo.md b/apps/docs/reference/api/generated/riviere-query/interfaces/EventHandlerInfo.md deleted file mode 100644 index cf9141183..000000000 --- a/apps/docs/reference/api/generated/riviere-query/interfaces/EventHandlerInfo.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: EventHandlerInfo - -Defined in: [packages/riviere-query/src/features/querying/queries/event-types.ts:121](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/event-types.ts#L121) - -Information about an event handler component. - -## Riviere-role - -query-model - -## Properties - -### domain - -> **domain**: `string` & `$brand`\<`"DomainName"`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/event-types.ts:127](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/event-types.ts#L127) - -The domain containing the handler. - -*** - -### handlerName - -> **handlerName**: `string` & `$brand`\<`"HandlerName"`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/event-types.ts:125](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/event-types.ts#L125) - -The handler's name. - -*** - -### id - -> **id**: `string` & `$brand`\<`"HandlerId"`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/event-types.ts:123](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/event-types.ts#L123) - -The handler's component ID. - -*** - -### subscribedEvents - -> **subscribedEvents**: `string` & `$brand`\<`"EventName"`\>[] - -Defined in: [packages/riviere-query/src/features/querying/queries/event-types.ts:129](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/event-types.ts#L129) - -List of event names this handler subscribes to. - -*** - -### subscribedEventsWithDomain - -> **subscribedEventsWithDomain**: [`SubscribedEventWithDomain`](../type-aliases/SubscribedEventWithDomain.md)[] - -Defined in: [packages/riviere-query/src/features/querying/queries/event-types.ts:131](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/event-types.ts#L131) - -Subscribed events with source domain information. diff --git a/apps/docs/reference/api/generated/riviere-query/interfaces/EventSubscriber.md b/apps/docs/reference/api/generated/riviere-query/interfaces/EventSubscriber.md deleted file mode 100644 index c78947d45..000000000 --- a/apps/docs/reference/api/generated/riviere-query/interfaces/EventSubscriber.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: EventSubscriber - -Defined in: [packages/riviere-query/src/features/querying/queries/event-types.ts:63](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/event-types.ts#L63) - -An event handler that subscribes to an event. - -## Riviere-role - -query-model - -## Properties - -### domain - -> **domain**: `string` & `$brand`\<`"DomainName"`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/event-types.ts:69](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/event-types.ts#L69) - -The domain containing the handler. - -*** - -### handlerId - -> **handlerId**: `string` & `$brand`\<`"HandlerId"`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/event-types.ts:65](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/event-types.ts#L65) - -The handler's component ID. - -*** - -### handlerName - -> **handlerName**: `string` & `$brand`\<`"HandlerName"`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/event-types.ts:67](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/event-types.ts#L67) - -The handler's name. diff --git a/apps/docs/reference/api/generated/riviere-query/interfaces/ExternalDomain.md b/apps/docs/reference/api/generated/riviere-query/interfaces/ExternalDomain.md deleted file mode 100644 index cd06b4dfe..000000000 --- a/apps/docs/reference/api/generated/riviere-query/interfaces/ExternalDomain.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: ExternalDomain - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:313](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L313) - -An external domain that components connect to. - -External domains are any systems not represented in the graph—third-party -services (Stripe, Twilio) or internal domains outside the current scope. - -## Riviere-role - -query-model - -## Properties - -### connectionCount - -> **connectionCount**: `number` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:319](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L319) - -Total number of connections to this external domain. - -*** - -### name - -> **name**: `string` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:315](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L315) - -Name of the external domain (e.g., "Stripe", "Twilio"). - -*** - -### sourceDomains - -> **sourceDomains**: `string` & `$brand`\<`"DomainName"`\>[] - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:317](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L317) - -Domains that have connections to this external domain. diff --git a/apps/docs/reference/api/generated/riviere-query/interfaces/Flow.md b/apps/docs/reference/api/generated/riviere-query/interfaces/Flow.md deleted file mode 100644 index 8fa4addc4..000000000 --- a/apps/docs/reference/api/generated/riviere-query/interfaces/Flow.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: Flow - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:243](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L243) - -An execution flow from entry point through the graph. - -## Riviere-role - -query-model - -## Properties - -### entryPoint - -> **entryPoint**: `Component` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:245](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L245) - -The entry point component. - -*** - -### steps - -> **steps**: [`FlowStep`](FlowStep.md)[] - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:247](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L247) - -Steps in the flow including entry point. diff --git a/apps/docs/reference/api/generated/riviere-query/interfaces/FlowStep.md b/apps/docs/reference/api/generated/riviere-query/interfaces/FlowStep.md deleted file mode 100644 index 612dd3914..000000000 --- a/apps/docs/reference/api/generated/riviere-query/interfaces/FlowStep.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: FlowStep - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:228](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L228) - -A step in an execution flow. - -## Riviere-role - -query-model - -## Properties - -### component - -> **component**: `Component` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:230](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L230) - -The component at this step. - -*** - -### depth - -> **depth**: `number` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:234](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L234) - -Depth from entry point (0 = entry point). - -*** - -### externalLinks - -> **externalLinks**: `ExternalLink`[] - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:236](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L236) - -External links from this component to external systems. - -*** - -### outgoingLinks - -> **outgoingLinks**: `Link`[] - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:232](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L232) - -Exact links leaving this component, preserving branching relationship semantics. diff --git a/apps/docs/reference/api/generated/riviere-query/interfaces/GraphDiff.md b/apps/docs/reference/api/generated/riviere-query/interfaces/GraphDiff.md deleted file mode 100644 index 56c78a668..000000000 --- a/apps/docs/reference/api/generated/riviere-query/interfaces/GraphDiff.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: GraphDiff - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:197](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L197) - -Complete diff between two graph versions. - -## Riviere-role - -query-model - -## Properties - -### components - -> **components**: `object` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:199](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L199) - -Component changes. - -#### added - -> **added**: `Component`[] - -Components present in new graph but not old. - -#### modified - -> **modified**: [`ComponentModification`](ComponentModification.md)[] - -Components present in both with different values. - -#### removed - -> **removed**: `Component`[] - -Components present in old graph but not new. - -*** - -### links - -> **links**: `object` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:208](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L208) - -Link changes. - -#### added - -> **added**: `Link`[] - -Links present in new graph but not old. - -#### removed - -> **removed**: `Link`[] - -Links present in old graph but not new. - -*** - -### stats - -> **stats**: [`DiffStats`](DiffStats.md) - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:215](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L215) - -Summary statistics. diff --git a/apps/docs/reference/api/generated/riviere-query/interfaces/GraphStats.md b/apps/docs/reference/api/generated/riviere-query/interfaces/GraphStats.md deleted file mode 100644 index e880e66e0..000000000 --- a/apps/docs/reference/api/generated/riviere-query/interfaces/GraphStats.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: GraphStats - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:291](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L291) - -Aggregate statistics about a graph. - -## Riviere-role - -query-model - -## Properties - -### apiCount - -> **apiCount**: `number` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:299](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L299) - -Number of API components. - -*** - -### componentCount - -> **componentCount**: `number` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:293](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L293) - -Total number of components. - -*** - -### domainCount - -> **domainCount**: `number` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:297](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L297) - -Number of domains. - -*** - -### entityCount - -> **entityCount**: `number` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:301](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L301) - -Number of unique entities. - -*** - -### eventCount - -> **eventCount**: `number` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:303](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L303) - -Number of Event components. - -*** - -### linkCount - -> **linkCount**: `number` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:295](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L295) - -Total number of links. diff --git a/apps/docs/reference/api/generated/riviere-query/interfaces/KnownSourceEvent.md b/apps/docs/reference/api/generated/riviere-query/interfaces/KnownSourceEvent.md deleted file mode 100644 index e84ef7d99..000000000 --- a/apps/docs/reference/api/generated/riviere-query/interfaces/KnownSourceEvent.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: KnownSourceEvent - -Defined in: [packages/riviere-query/src/features/querying/queries/event-types.ts:91](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/event-types.ts#L91) - -A subscribed event where the source domain is known. - -## Riviere-role - -query-model - -## Properties - -### eventName - -> **eventName**: `string` & `$brand`\<`"EventName"`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/event-types.ts:93](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/event-types.ts#L93) - -The event name. - -*** - -### sourceDomain - -> **sourceDomain**: `string` & `$brand`\<`"DomainName"`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/event-types.ts:95](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/event-types.ts#L95) - -The domain that publishes this event. - -*** - -### sourceKnown - -> **sourceKnown**: `true` - -Defined in: [packages/riviere-query/src/features/querying/queries/event-types.ts:97](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/event-types.ts#L97) - -Indicates the source is known. diff --git a/apps/docs/reference/api/generated/riviere-query/interfaces/PublishedEvent.md b/apps/docs/reference/api/generated/riviere-query/interfaces/PublishedEvent.md deleted file mode 100644 index 4efbb16ff..000000000 --- a/apps/docs/reference/api/generated/riviere-query/interfaces/PublishedEvent.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: PublishedEvent - -Defined in: [packages/riviere-query/src/features/querying/queries/event-types.ts:76](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/event-types.ts#L76) - -A published event with its subscribers. - -## Riviere-role - -query-model - -## Properties - -### domain - -> **domain**: `string` & `$brand`\<`"DomainName"`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/event-types.ts:82](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/event-types.ts#L82) - -The domain that publishes the event. - -*** - -### eventName - -> **eventName**: `string` & `$brand`\<`"EventName"`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/event-types.ts:80](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/event-types.ts#L80) - -The event name. - -*** - -### handlers - -> **handlers**: [`EventSubscriber`](EventSubscriber.md)[] - -Defined in: [packages/riviere-query/src/features/querying/queries/event-types.ts:84](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/event-types.ts#L84) - -Event handlers subscribed to this event. - -*** - -### id - -> **id**: `string` & `$brand`\<`"EventId"`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/event-types.ts:78](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/event-types.ts#L78) - -The event component's ID. diff --git a/apps/docs/reference/api/generated/riviere-query/interfaces/SearchWithFlowOptions.md b/apps/docs/reference/api/generated/riviere-query/interfaces/SearchWithFlowOptions.md deleted file mode 100644 index da63e376b..000000000 --- a/apps/docs/reference/api/generated/riviere-query/interfaces/SearchWithFlowOptions.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: SearchWithFlowOptions - -Defined in: [packages/riviere-query/src/features/querying/queries/flow-queries.ts:138](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/flow-queries.ts#L138) - -## Riviere-role - -query-model-use-case-input - -## Properties - -### returnAllOnEmptyQuery - -> **returnAllOnEmptyQuery**: `boolean` - -Defined in: [packages/riviere-query/src/features/querying/queries/flow-queries.ts:138](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/flow-queries.ts#L138) diff --git a/apps/docs/reference/api/generated/riviere-query/interfaces/SearchWithFlowResult.md b/apps/docs/reference/api/generated/riviere-query/interfaces/SearchWithFlowResult.md deleted file mode 100644 index 30a93ecca..000000000 --- a/apps/docs/reference/api/generated/riviere-query/interfaces/SearchWithFlowResult.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: SearchWithFlowResult - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:254](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L254) - -Result of searchWithFlow containing matches and their flow context. - -## Riviere-role - -query-model - -## Properties - -### matchingIds - -> **matchingIds**: `string` & `$brand`\<`"ComponentId"`\>[] - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:256](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L256) - -IDs of components that matched the search. - -*** - -### visibleIds - -> **visibleIds**: `string` & `$brand`\<`"ComponentId"`\>[] - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:258](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L258) - -IDs of all components visible in the matching flows. diff --git a/apps/docs/reference/api/generated/riviere-query/interfaces/UnknownSourceEvent.md b/apps/docs/reference/api/generated/riviere-query/interfaces/UnknownSourceEvent.md deleted file mode 100644 index 22e281ed9..000000000 --- a/apps/docs/reference/api/generated/riviere-query/interfaces/UnknownSourceEvent.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: UnknownSourceEvent - -Defined in: [packages/riviere-query/src/features/querying/queries/event-types.ts:104](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/event-types.ts#L104) - -A subscribed event where the source domain is unknown. - -## Riviere-role - -query-model - -## Properties - -### eventName - -> **eventName**: `string` & `$brand`\<`"EventName"`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/event-types.ts:106](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/event-types.ts#L106) - -The event name. - -*** - -### sourceKnown - -> **sourceKnown**: `false` - -Defined in: [packages/riviere-query/src/features/querying/queries/event-types.ts:108](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/event-types.ts#L108) - -Indicates the source is unknown. diff --git a/apps/docs/reference/api/generated/riviere-query/interfaces/ValidationError.md b/apps/docs/reference/api/generated/riviere-query/interfaces/ValidationError.md deleted file mode 100644 index 028c5e287..000000000 --- a/apps/docs/reference/api/generated/riviere-query/interfaces/ValidationError.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: ValidationError - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:103](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L103) - -A validation error found in the graph. - -## Riviere-role - -query-model - -## Properties - -### code - -> **code**: [`ValidationErrorCode`](../type-aliases/ValidationErrorCode.md) - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:109](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L109) - -Machine-readable error code. - -*** - -### message - -> **message**: `string` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:107](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L107) - -Human-readable error description. - -*** - -### path - -> **path**: `string` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:105](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L105) - -JSON path to the error location. diff --git a/apps/docs/reference/api/generated/riviere-query/interfaces/ValidationResult.md b/apps/docs/reference/api/generated/riviere-query/interfaces/ValidationResult.md deleted file mode 100644 index 94197a6bf..000000000 --- a/apps/docs/reference/api/generated/riviere-query/interfaces/ValidationResult.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -pageClass: reference ---- - -# Interface: ValidationResult - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:116](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L116) - -Result of graph validation. - -## Riviere-role - -query-model - -## Properties - -### errors - -> **errors**: [`ValidationError`](ValidationError.md)[] - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:120](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L120) - -List of validation errors (empty if valid). - -*** - -### valid - -> **valid**: `boolean` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:118](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L118) - -Whether the graph passed validation. diff --git a/apps/docs/reference/api/generated/riviere-query/type-aliases/ComponentId.md b/apps/docs/reference/api/generated/riviere-query/type-aliases/ComponentId.md deleted file mode 100644 index 3fd87e30e..000000000 --- a/apps/docs/reference/api/generated/riviere-query/type-aliases/ComponentId.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -pageClass: reference ---- - -# Type Alias: ComponentId - -> **ComponentId** = `z.infer`\<*typeof* `componentIdSchema`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:31](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L31) - -Branded type for component identifiers. - -## Riviere-role - -query-model diff --git a/apps/docs/reference/api/generated/riviere-query/type-aliases/DomainName.md b/apps/docs/reference/api/generated/riviere-query/type-aliases/DomainName.md deleted file mode 100644 index 3da2f3982..000000000 --- a/apps/docs/reference/api/generated/riviere-query/type-aliases/DomainName.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -pageClass: reference ---- - -# Type Alias: DomainName - -> **DomainName** = `z.infer`\<*typeof* `domainNameSchema`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:49](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L49) - -Branded type for domain names. - -## Riviere-role - -query-model diff --git a/apps/docs/reference/api/generated/riviere-query/type-aliases/EntityName.md b/apps/docs/reference/api/generated/riviere-query/type-aliases/EntityName.md deleted file mode 100644 index 71d3ab936..000000000 --- a/apps/docs/reference/api/generated/riviere-query/type-aliases/EntityName.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -pageClass: reference ---- - -# Type Alias: EntityName - -> **EntityName** = `z.infer`\<*typeof* `entityNameSchema`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:43](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L43) - -Branded type for entity names. - -## Riviere-role - -query-model diff --git a/apps/docs/reference/api/generated/riviere-query/type-aliases/EventId.md b/apps/docs/reference/api/generated/riviere-query/type-aliases/EventId.md deleted file mode 100644 index de9b92799..000000000 --- a/apps/docs/reference/api/generated/riviere-query/type-aliases/EventId.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -pageClass: reference ---- - -# Type Alias: EventId - -> **EventId** = `z.infer`\<*typeof* `eventIdSchema`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:67](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L67) - -Branded type for event identifiers. - -## Riviere-role - -query-model diff --git a/apps/docs/reference/api/generated/riviere-query/type-aliases/EventName.md b/apps/docs/reference/api/generated/riviere-query/type-aliases/EventName.md deleted file mode 100644 index 6ef7c46e0..000000000 --- a/apps/docs/reference/api/generated/riviere-query/type-aliases/EventName.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -pageClass: reference ---- - -# Type Alias: EventName - -> **EventName** = `z.infer`\<*typeof* `eventNameSchema`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:73](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L73) - -Branded type for event names. - -## Riviere-role - -query-model diff --git a/apps/docs/reference/api/generated/riviere-query/type-aliases/HandlerId.md b/apps/docs/reference/api/generated/riviere-query/type-aliases/HandlerId.md deleted file mode 100644 index 9af924116..000000000 --- a/apps/docs/reference/api/generated/riviere-query/type-aliases/HandlerId.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -pageClass: reference ---- - -# Type Alias: HandlerId - -> **HandlerId** = `z.infer`\<*typeof* `handlerIdSchema`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:79](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L79) - -Branded type for event handler identifiers. - -## Riviere-role - -query-model diff --git a/apps/docs/reference/api/generated/riviere-query/type-aliases/HandlerName.md b/apps/docs/reference/api/generated/riviere-query/type-aliases/HandlerName.md deleted file mode 100644 index 20792ac91..000000000 --- a/apps/docs/reference/api/generated/riviere-query/type-aliases/HandlerName.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -pageClass: reference ---- - -# Type Alias: HandlerName - -> **HandlerName** = `z.infer`\<*typeof* `handlerNameSchema`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:85](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L85) - -Branded type for event handler names. - -## Riviere-role - -query-model diff --git a/apps/docs/reference/api/generated/riviere-query/type-aliases/LinkId.md b/apps/docs/reference/api/generated/riviere-query/type-aliases/LinkId.md deleted file mode 100644 index 593f6ef81..000000000 --- a/apps/docs/reference/api/generated/riviere-query/type-aliases/LinkId.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -pageClass: reference ---- - -# Type Alias: LinkId - -> **LinkId** = `z.infer`\<*typeof* `linkIdSchema`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:37](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L37) - -Branded type for link identifiers. - -## Riviere-role - -query-model diff --git a/apps/docs/reference/api/generated/riviere-query/type-aliases/LinkType.md b/apps/docs/reference/api/generated/riviere-query/type-aliases/LinkType.md deleted file mode 100644 index fea9a79b5..000000000 --- a/apps/docs/reference/api/generated/riviere-query/type-aliases/LinkType.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -pageClass: reference ---- - -# Type Alias: LinkType - -> **LinkType** = `"sync"` \| `"async"` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:222](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L222) - -Type of link between components. - -## Riviere-role - -query-model diff --git a/apps/docs/reference/api/generated/riviere-query/type-aliases/OperationName.md b/apps/docs/reference/api/generated/riviere-query/type-aliases/OperationName.md deleted file mode 100644 index 169fc8e0a..000000000 --- a/apps/docs/reference/api/generated/riviere-query/type-aliases/OperationName.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -pageClass: reference ---- - -# Type Alias: OperationName - -> **OperationName** = `z.infer`\<*typeof* `operationNameSchema`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:61](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L61) - -Branded type for operation names. - -## Riviere-role - -query-model diff --git a/apps/docs/reference/api/generated/riviere-query/type-aliases/State.md b/apps/docs/reference/api/generated/riviere-query/type-aliases/State.md deleted file mode 100644 index 580702c01..000000000 --- a/apps/docs/reference/api/generated/riviere-query/type-aliases/State.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -pageClass: reference ---- - -# Type Alias: State - -> **State** = `z.infer`\<*typeof* `stateSchema`\> - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:55](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L55) - -Branded type for state names in entity state machines. - -## Riviere-role - -query-model diff --git a/apps/docs/reference/api/generated/riviere-query/type-aliases/SubscribedEventWithDomain.md b/apps/docs/reference/api/generated/riviere-query/type-aliases/SubscribedEventWithDomain.md deleted file mode 100644 index 609c79adc..000000000 --- a/apps/docs/reference/api/generated/riviere-query/type-aliases/SubscribedEventWithDomain.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -pageClass: reference ---- - -# Type Alias: SubscribedEventWithDomain - -> **SubscribedEventWithDomain** = [`KnownSourceEvent`](../interfaces/KnownSourceEvent.md) \| [`UnknownSourceEvent`](../interfaces/UnknownSourceEvent.md) - -Defined in: [packages/riviere-query/src/features/querying/queries/event-types.ts:115](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/event-types.ts#L115) - -A subscribed event with optional source domain information. - -## Riviere-role - -query-model diff --git a/apps/docs/reference/api/generated/riviere-query/type-aliases/ValidationErrorCode.md b/apps/docs/reference/api/generated/riviere-query/type-aliases/ValidationErrorCode.md deleted file mode 100644 index 704087ede..000000000 --- a/apps/docs/reference/api/generated/riviere-query/type-aliases/ValidationErrorCode.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -pageClass: reference ---- - -# Type Alias: ValidationErrorCode - -> **ValidationErrorCode** = `"INVALID_LINK_SOURCE"` \| `"INVALID_LINK_TARGET"` \| `"INVALID_TYPE"` \| `"INVALID_RELATIONSHIP_TYPE"` \| `"DUPLICATE_LINK_ID"` \| `"DUPLICATE_LINK"` - -Defined in: [packages/riviere-query/src/features/querying/queries/domain-types.ts:91](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-query/src/features/querying/queries/domain-types.ts#L91) - -Error codes for graph validation failures. - -## Riviere-role - -query-model diff --git a/apps/docs/reference/api/index.md b/apps/docs/reference/api/index.md index 6fba30ec3..9d0bc4c53 100644 --- a/apps/docs/reference/api/index.md +++ b/apps/docs/reference/api/index.md @@ -5,7 +5,7 @@ The main class. Create an instance, add components, connect them, build a graph. ```typescript -import { RiviereBuilder } from '@living-architecture/riviere-builder' +import { RiviereBuilder } from '@living-architecture/riviere-builder-domain-model' const builder = RiviereBuilder.new({ sources: [{ type: 'git', url: 'https://github.com/your-org/your-repo' }], @@ -64,6 +64,8 @@ builder.link({ from: api.id, to: useCase.id, type: 'sync' }) Query and analyze Riviere graphs. ```typescript +import { RiviereQuery } from '@living-architecture/riviere-builder-domain-model' + // From builder const query = builder.query() @@ -75,40 +77,40 @@ const query = RiviereQuery.fromJSON(jsonContents) | Method | Purpose | |--------|---------| -| [`components()`](/reference/api/generated/riviere-query/classes/RiviereQuery#components) | Get all components | -| [`componentById(id)`](/reference/api/generated/riviere-query/classes/RiviereQuery#componentbyid) | Find component by ID | -| [`componentsInDomain(domainId)`](/reference/api/generated/riviere-query/classes/RiviereQuery#componentsindomain) | Components in a domain | -| [`componentsByType(type)`](/reference/api/generated/riviere-query/classes/RiviereQuery#componentsbytype) | Components of a type | -| [`find(predicate)`](/reference/api/generated/riviere-query/classes/RiviereQuery#find) | Find with custom function | -| [`findAll(predicate)`](/reference/api/generated/riviere-query/classes/RiviereQuery#findall) | Find all matching | +| [`components()`](/reference/api/generated/riviere-builder/classes/RiviereQuery#components) | Get all components | +| [`componentById(id)`](/reference/api/generated/riviere-builder/classes/RiviereQuery#componentbyid) | Find component by ID | +| [`componentsInDomain(domainId)`](/reference/api/generated/riviere-builder/classes/RiviereQuery#componentsindomain) | Components in a domain | +| [`componentsByType(type)`](/reference/api/generated/riviere-builder/classes/RiviereQuery#componentsbytype) | Components of a type | +| [`find(predicate)`](/reference/api/generated/riviere-builder/classes/RiviereQuery#find) | Find with custom function | +| [`findAll(predicate)`](/reference/api/generated/riviere-builder/classes/RiviereQuery#findall) | Find all matching | ### Link methods | Method | Purpose | |--------|---------| -| [`links()`](/reference/api/generated/riviere-query/classes/RiviereQuery#links) | Get all links | +| [`links()`](/reference/api/generated/riviere-builder/classes/RiviereQuery#links) | Get all links | ### Domain methods | Method | Purpose | |--------|---------| -| [`domains()`](/reference/api/generated/riviere-query/classes/RiviereQuery#domains) | Domain info with component counts | -| [`entities(domainId?)`](/reference/api/generated/riviere-query/classes/RiviereQuery#entities) | Entity names in domain | -| [`crossDomainLinks(domainId)`](/reference/api/generated/riviere-query/classes/RiviereQuery#crossdomainlinks) | Links leaving domain | +| [`domains()`](/reference/api/generated/riviere-builder/classes/RiviereQuery#domains) | Domain info with component counts | +| [`entities(domainId?)`](/reference/api/generated/riviere-builder/classes/RiviereQuery#entities) | Entity names in domain | +| [`crossDomainLinks(domainId)`](/reference/api/generated/riviere-builder/classes/RiviereQuery#crossdomainlinks) | Links leaving domain | ### Analysis methods | Method | Purpose | |--------|---------| -| [`entryPoints()`](/reference/api/generated/riviere-query/classes/RiviereQuery#entrypoints) | UI/API/EventHandler with no incoming links | +| [`entryPoints()`](/reference/api/generated/riviere-builder/classes/RiviereQuery#entrypoints) | UI/API/EventHandler with no incoming links | --- ## Full Documentation - [RiviereBuilder](/reference/api/generated/riviere-builder/classes/RiviereBuilder) — Build graphs with type-safe methods -- [RiviereQuery](/reference/api/generated/riviere-query/classes/RiviereQuery) — Query and analyze graphs -- [Types](/reference/api/generated/riviere-query/README) — All TypeScript type definitions +- [RiviereQuery](/reference/api/generated/riviere-builder/classes/RiviereQuery) — Query and analyze graphs +- [Types](/reference/api/generated/riviere-builder/README) — All TypeScript type definitions ## See Also diff --git a/apps/docs/reference/extraction-config/connections.md b/apps/docs/reference/extraction-config/connections.md index 85281d7da..154991829 100644 --- a/apps/docs/reference/extraction-config/connections.md +++ b/apps/docs/reference/extraction-config/connections.md @@ -74,13 +74,13 @@ modules: name: orders path: ../../orders modules: /src/{module} - extends: '@living-architecture/riviere-extract-conventions' + extends: '@living-architecture/riviere-extract-conventions-published-language' - domain: bff name: bff path: ../../bff modules: /src/{module} - extends: '@living-architecture/riviere-extract-conventions' + extends: '@living-architecture/riviere-extract-conventions-published-language' connections: eventPublishers: diff --git a/apps/docs/reference/extraction-config/decorators.md b/apps/docs/reference/extraction-config/decorators.md index f3c053065..e9662a76b 100644 --- a/apps/docs/reference/extraction-config/decorators.md +++ b/apps/docs/reference/extraction-config/decorators.md @@ -5,7 +5,7 @@ Annotate TypeScript code with architectural component markers. ## Installation ```bash -npm install --save-dev @living-architecture/riviere-extract-conventions +npm install --save-dev @living-architecture/riviere-extract-conventions-published-language ``` ## Decorator Categories @@ -24,7 +24,7 @@ All public methods in the class become separate components. ### @APIContainer ```typescript -import { APIContainer, APIEndpoint } from '@living-architecture/riviere-extract-conventions' +import { APIContainer, APIEndpoint } from '@living-architecture/riviere-extract-conventions-published-language' @APIContainer class OrderController { @@ -43,7 +43,7 @@ class OrderController { ### @DomainOpContainer ```typescript -import { DomainOpContainer, DomainOp } from '@living-architecture/riviere-extract-conventions' +import { DomainOpContainer, DomainOp } from '@living-architecture/riviere-extract-conventions-published-language' @DomainOpContainer class Order { @@ -62,7 +62,7 @@ class Order { ### @EventHandlerContainer ```typescript -import { EventHandlerContainer, EventHandler } from '@living-architecture/riviere-extract-conventions' +import { EventHandlerContainer, EventHandler } from '@living-architecture/riviere-extract-conventions-published-language' @EventHandlerContainer class OrderEventHandlers { @@ -80,7 +80,7 @@ The class itself is the architectural component. ### @UseCase ```typescript -import { UseCase } from '@living-architecture/riviere-extract-conventions' +import { UseCase } from '@living-architecture/riviere-extract-conventions-published-language' @UseCase class PlaceOrderUseCase { @@ -93,7 +93,7 @@ class PlaceOrderUseCase { ### @Event ```typescript -import { Event } from '@living-architecture/riviere-extract-conventions' +import { Event } from '@living-architecture/riviere-extract-conventions-published-language' @Event class OrderPlaced { @@ -108,7 +108,7 @@ class OrderPlaced { ### @UI ```typescript -import { UI } from '@living-architecture/riviere-extract-conventions' +import { UI } from '@living-architecture/riviere-extract-conventions-published-language' @UI class OrderList { @@ -125,7 +125,7 @@ Individual methods marked as components (without container decorator on class). ### @APIEndpoint ```typescript -import { APIEndpoint } from '@living-architecture/riviere-extract-conventions' +import { APIEndpoint } from '@living-architecture/riviere-extract-conventions-published-language' class OrderController { @APIEndpoint @@ -143,7 +143,7 @@ class OrderController { ### @DomainOp ```typescript -import { DomainOp } from '@living-architecture/riviere-extract-conventions' +import { DomainOp } from '@living-architecture/riviere-extract-conventions-published-language' class Order { @DomainOp @@ -161,7 +161,7 @@ class Order { ### @EventHandler ```typescript -import { EventHandler } from '@living-architecture/riviere-extract-conventions' +import { EventHandler } from '@living-architecture/riviere-extract-conventions-published-language' class OrderHandlers { @EventHandler @@ -178,7 +178,7 @@ class OrderHandlers { Define custom component types: ```typescript -import { Custom } from '@living-architecture/riviere-extract-conventions' +import { Custom } from '@living-architecture/riviere-extract-conventions-published-language' @Custom('saga') class OrderSaga { @@ -191,7 +191,7 @@ class OrderSaga { Exclude classes from extraction: ```typescript -import { Ignore } from '@living-architecture/riviere-extract-conventions' +import { Ignore } from '@living-architecture/riviere-extract-conventions-published-language' @Ignore class TestHelper { @@ -221,7 +221,7 @@ The conventions package provides a default config that detects all decorator typ modules: - name: "orders" path: "src/orders/**/*.ts" - extends: "@living-architecture/riviere-extract-conventions" + extends: "@living-architecture/riviere-extract-conventions-published-language" ``` [View Default Config Source](https://github.com/NTCoding/living-architecture/blob/main/packages/riviere-extract-conventions/src/default-extraction.config.json) diff --git a/apps/docs/reference/extraction-config/examples.md b/apps/docs/reference/extraction-config/examples.md index d9a0d2f9a..650a2eec5 100644 --- a/apps/docs/reference/extraction-config/examples.md +++ b/apps/docs/reference/extraction-config/examples.md @@ -266,13 +266,13 @@ modules: inClassWith: hasDecorator: name: "APIContainer" - from: "@living-architecture/riviere-extract-conventions" + from: "@living-architecture/riviere-extract-conventions-published-language" useCase: find: "classes" where: hasDecorator: name: "UseCase" - from: "@living-architecture/riviere-extract-conventions" + from: "@living-architecture/riviere-extract-conventions-published-language" domainOp: { notUsed: true } event: { notUsed: true } eventHandler: { notUsed: true } @@ -345,20 +345,20 @@ modules: # All modules inherit detection rules from the conventions package - name: "orders" path: "src/orders/**/*.ts" - extends: "@living-architecture/riviere-extract-conventions" + extends: "@living-architecture/riviere-extract-conventions-published-language" - name: "shipping" path: "src/shipping/**/*.ts" - extends: "@living-architecture/riviere-extract-conventions" + extends: "@living-architecture/riviere-extract-conventions-published-language" - name: "inventory" path: "src/inventory/**/*.ts" - extends: "@living-architecture/riviere-extract-conventions" + extends: "@living-architecture/riviere-extract-conventions-published-language" # Override specific rules when needed - name: "payments" path: "src/payments/**/*.ts" - extends: "@living-architecture/riviere-extract-conventions" + extends: "@living-architecture/riviere-extract-conventions-published-language" event: { notUsed: true } # No events in payments module ui: { notUsed: true } # No UI in payments module ``` diff --git a/apps/docs/scripts/validate-cli-refs.ts b/apps/docs/scripts/validate-cli-refs.ts index ed5a8f861..4484a0aef 100644 --- a/apps/docs/scripts/validate-cli-refs.ts +++ b/apps/docs/scripts/validate-cli-refs.ts @@ -4,11 +4,9 @@ * Fails build if docs reference non-existent commands. */ -import { - readdirSync, readFileSync, statSync -} from 'node:fs' +import { readdirSync, readFileSync, statSync } from 'node:fs' import { join } from 'node:path' -import { createProgram } from '../../../packages/riviere-cli/src/shell/cli' +import { createProgram } from '../../cli/src/shell/cli' interface ValidationError { file: string diff --git a/apps/docs/tsconfig.app.json b/apps/docs/tsconfig.app.json index 02b1d3d47..66436ab53 100644 --- a/apps/docs/tsconfig.app.json +++ b/apps/docs/tsconfig.app.json @@ -11,7 +11,7 @@ "exclude": [".vitepress/**/*.spec.ts"], "references": [ { - "path": "../../packages/riviere-cli/tsconfig.lib.json" + "path": "../cli/tsconfig.lib.json" }, { "path": "./tsconfig.scripts.json" diff --git a/apps/eclair/package.json b/apps/eclair/package.json index 9ec9c895b..07e4351fa 100644 --- a/apps/eclair/package.json +++ b/apps/eclair/package.json @@ -49,8 +49,8 @@ } }, "dependencies": { - "@living-architecture/riviere-query": "workspace:*", - "@living-architecture/riviere-schema": "workspace:*", + "@living-architecture/riviere-builder-domain-model": "workspace:*", + "@living-architecture/riviere-schema-published-language": "workspace:*", "@xyflow/react": "^12.9.3", "d3": "^7.9.0", "dagre": "^0.8.5", diff --git a/apps/eclair/src/features/comparison/components/UploadZone.tsx b/apps/eclair/src/features/comparison/components/UploadZone.tsx index 0c2121690..1169a1297 100644 --- a/apps/eclair/src/features/comparison/components/UploadZone.tsx +++ b/apps/eclair/src/features/comparison/components/UploadZone.tsx @@ -1,7 +1,7 @@ import { useCallback, useRef } from 'react' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' interface UploadedFile { readonly name: string diff --git a/apps/eclair/src/features/comparison/entrypoint/ComparisonPage.tsx b/apps/eclair/src/features/comparison/entrypoint/ComparisonPage.tsx index dbc8b2b57..069bced13 100644 --- a/apps/eclair/src/features/comparison/entrypoint/ComparisonPage.tsx +++ b/apps/eclair/src/features/comparison/entrypoint/ComparisonPage.tsx @@ -2,7 +2,7 @@ import { useState, useCallback } from 'react' import type { Node } from '../queries/eclair-types' -import { parseRiviereGraph } from '@living-architecture/riviere-schema' +import { parseRiviereGraph } from '@living-architecture/riviere-schema-published-language/validation' import { compareGraphs, type GraphDiff } from '../queries/compare-graphs' @@ -59,12 +59,18 @@ function buildChangeItems(diff: GraphDiff): ChangeItemBase[] { function parseGraphFile(content: string, fileName: string): UploadState { try { const data: unknown = JSON.parse(content) - const graph = parseRiviereGraph(data) + const result = parseRiviereGraph(data) + if (!result.success) { + return { + status: 'error', + error: { message: result.issues.join('\n') }, + } + } return { status: 'loaded', file: { name: fileName, - graph, + graph: result.graph, }, } } catch (e) { diff --git a/apps/eclair/src/features/comparison/queries/compare-graphs.spec.ts b/apps/eclair/src/features/comparison/queries/compare-graphs.spec.ts index 0cd810345..8c86bc540 100644 --- a/apps/eclair/src/features/comparison/queries/compare-graphs.spec.ts +++ b/apps/eclair/src/features/comparison/queries/compare-graphs.spec.ts @@ -1,11 +1,7 @@ -import { - describe, it, expect -} from 'vitest' +import { describe, it, expect } from 'vitest' import { compareGraphs } from './compare-graphs' -import type { RiviereGraph } from '@living-architecture/riviere-schema' -import type { - Node, Edge -} from '@/platform/domain/eclair-types' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' +import type { Node, Edge } from '@/platform/domain/eclair-types' import { parseNode, parseEdge, diff --git a/apps/eclair/src/features/comparison/queries/compare-graphs.ts b/apps/eclair/src/features/comparison/queries/compare-graphs.ts index 35f12ce26..dcaed1274 100644 --- a/apps/eclair/src/features/comparison/queries/compare-graphs.ts +++ b/apps/eclair/src/features/comparison/queries/compare-graphs.ts @@ -1,12 +1,14 @@ -import type { RiviereGraph } from '@living-architecture/riviere-schema' -import type { - Node, Edge, NodeType, NodeId -} from '@/platform/domain/eclair-types' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' +import type { Node, Edge, NodeType, NodeId } from '@/platform/domain/eclair-types' import { GraphError } from '@/platform/infra/errors/errors' -interface NodeAddition {node: Node} +interface NodeAddition { + node: Node +} -interface NodeRemoval {node: Node} +interface NodeRemoval { + node: Node +} interface NodeModification { before: Node @@ -14,9 +16,13 @@ interface NodeModification { changedFields: string[] } -interface EdgeAddition {edge: Edge} +interface EdgeAddition { + edge: Edge +} -interface EdgeRemoval {edge: Edge} +interface EdgeRemoval { + edge: Edge +} interface EdgeModification { before: Edge @@ -61,9 +67,13 @@ export interface NodeTypeChanges { modified: NodeModification[] } -interface ByDomainAccumulator {data: Record} +interface ByDomainAccumulator { + data: Record +} -interface ByNodeTypeAccumulator {data: Map} +interface ByNodeTypeAccumulator { + data: Map +} export interface GraphDiff { nodes: NodeDiff diff --git a/apps/eclair/src/features/comparison/queries/compute-domain-connection-diff.edgeDetails.spec.ts b/apps/eclair/src/features/comparison/queries/compute-domain-connection-diff.edgeDetails.spec.ts index 6e2fca907..106e59555 100644 --- a/apps/eclair/src/features/comparison/queries/compute-domain-connection-diff.edgeDetails.spec.ts +++ b/apps/eclair/src/features/comparison/queries/compute-domain-connection-diff.edgeDetails.spec.ts @@ -1,8 +1,6 @@ -import { - describe, it, expect -} from 'vitest' +import { describe, it, expect } from 'vitest' import { computeDomainConnectionDiff } from './compute-domain-connection-diff' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import { parseNode, parseEdge, diff --git a/apps/eclair/src/features/comparison/queries/compute-domain-connection-diff.spec.ts b/apps/eclair/src/features/comparison/queries/compute-domain-connection-diff.spec.ts index c2c157acc..1fec4a6a5 100644 --- a/apps/eclair/src/features/comparison/queries/compute-domain-connection-diff.spec.ts +++ b/apps/eclair/src/features/comparison/queries/compute-domain-connection-diff.spec.ts @@ -1,8 +1,6 @@ -import { - describe, it, expect -} from 'vitest' +import { describe, it, expect } from 'vitest' import { computeDomainConnectionDiff } from './compute-domain-connection-diff' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import { parseNode, parseEdge, diff --git a/apps/eclair/src/features/comparison/queries/compute-domain-connection-diff.ts b/apps/eclair/src/features/comparison/queries/compute-domain-connection-diff.ts index 1be085788..c551c7750 100644 --- a/apps/eclair/src/features/comparison/queries/compute-domain-connection-diff.ts +++ b/apps/eclair/src/features/comparison/queries/compute-domain-connection-diff.ts @@ -1,7 +1,5 @@ -import type { RiviereGraph } from '@living-architecture/riviere-schema' -import type { - Node, NodeType -} from '@/platform/domain/eclair-types' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' +import type { Node, NodeType } from '@/platform/domain/eclair-types' export interface EdgeDetail { sourceNodeName: string diff --git a/apps/eclair/src/features/domain-map/entrypoint/DomainMapPage.spec.tsx b/apps/eclair/src/features/domain-map/entrypoint/DomainMapPage.spec.tsx index 7e0360bfd..b1d6838ba 100644 --- a/apps/eclair/src/features/domain-map/entrypoint/DomainMapPage.spec.tsx +++ b/apps/eclair/src/features/domain-map/entrypoint/DomainMapPage.spec.tsx @@ -8,7 +8,7 @@ import { MemoryRouter } from 'react-router-dom' import { DomainMapPage } from './DomainMapPage' import { ExportProvider } from '@/platform/infra/export/ExportContext' import { ThemeProvider } from '@/platform/infra/theme/ThemeContext' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import { parseNode, parseEdge, parseDomainMetadata } from '@/platform/infra/__fixtures__/riviere-test-fixtures' diff --git a/apps/eclair/src/features/domain-map/entrypoint/DomainMapPage.tsx b/apps/eclair/src/features/domain-map/entrypoint/DomainMapPage.tsx index a40fa3061..ce28e97ac 100644 --- a/apps/eclair/src/features/domain-map/entrypoint/DomainMapPage.tsx +++ b/apps/eclair/src/features/domain-map/entrypoint/DomainMapPage.tsx @@ -12,7 +12,7 @@ import type { Node, Edge, NodeMouseHandler, EdgeMouseHandler } from '@xyflow/react' import '@xyflow/react/dist/style.css' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import { useExport } from '@/platform/infra/export/ExportContext' import { generateExportFilename, diff --git a/apps/eclair/src/features/domain-map/queries/edge-aggregation.ts b/apps/eclair/src/features/domain-map/queries/edge-aggregation.ts index 4a57fca7f..2753ca3d0 100644 --- a/apps/eclair/src/features/domain-map/queries/edge-aggregation.ts +++ b/apps/eclair/src/features/domain-map/queries/edge-aggregation.ts @@ -1,4 +1,4 @@ -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import { getEdgeType, recordEdgeAggregation, diff --git a/apps/eclair/src/features/domain-map/queries/external-domain-handling.ts b/apps/eclair/src/features/domain-map/queries/external-domain-handling.ts index c8c0ff5af..dab0faf65 100644 --- a/apps/eclair/src/features/domain-map/queries/external-domain-handling.ts +++ b/apps/eclair/src/features/domain-map/queries/external-domain-handling.ts @@ -1,7 +1,5 @@ -import type { RiviereGraph } from '@living-architecture/riviere-schema' -import { - getEdgeType, type ConnectionDetail -} from './edgeAggregation' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' +import { getEdgeType, type ConnectionDetail } from './edgeAggregation' export interface ExternalEdgeInfo { targetName: string diff --git a/apps/eclair/src/features/domain-map/queries/extract-domain-map.external.spec.ts b/apps/eclair/src/features/domain-map/queries/extract-domain-map.external.spec.ts index 80588bef3..ecbef71ef 100644 --- a/apps/eclair/src/features/domain-map/queries/extract-domain-map.external.spec.ts +++ b/apps/eclair/src/features/domain-map/queries/extract-domain-map.external.spec.ts @@ -1,11 +1,7 @@ -import { - describe, it, expect -} from 'vitest' +import { describe, it, expect } from 'vitest' import { extractDomainMap } from './extract-domain-map' -import type { RiviereGraph } from '@living-architecture/riviere-schema' -import { - parseNode, parseDomainMetadata -} from '@/platform/infra/__fixtures__/riviere-test-fixtures' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' +import { parseNode, parseDomainMetadata } from '@/platform/infra/__fixtures__/riviere-test-fixtures' const testSourceLocation = { repository: 'test-repo', diff --git a/apps/eclair/src/features/domain-map/queries/extract-domain-map.reactflow.spec.ts b/apps/eclair/src/features/domain-map/queries/extract-domain-map.reactflow.spec.ts index 4fa467d53..97e5326e4 100644 --- a/apps/eclair/src/features/domain-map/queries/extract-domain-map.reactflow.spec.ts +++ b/apps/eclair/src/features/domain-map/queries/extract-domain-map.reactflow.spec.ts @@ -1,8 +1,6 @@ -import { - describe, it, expect -} from 'vitest' +import { describe, it, expect } from 'vitest' import { extractDomainMap } from './extract-domain-map' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import { parseNode, parseEdge, diff --git a/apps/eclair/src/features/domain-map/queries/extract-domain-map.spec.ts b/apps/eclair/src/features/domain-map/queries/extract-domain-map.spec.ts index af0509eb1..4b74b06ea 100644 --- a/apps/eclair/src/features/domain-map/queries/extract-domain-map.spec.ts +++ b/apps/eclair/src/features/domain-map/queries/extract-domain-map.spec.ts @@ -1,11 +1,7 @@ -import { - describe, it, expect -} from 'vitest' -import { - extractDomainMap, getConnectedDomains -} from './extract-domain-map' +import { describe, it, expect } from 'vitest' +import { extractDomainMap, getConnectedDomains } from './extract-domain-map' import type { DomainEdge } from './extract-domain-map' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import { parseNode, parseEdge, diff --git a/apps/eclair/src/features/domain-map/queries/extract-domain-map.ts b/apps/eclair/src/features/domain-map/queries/extract-domain-map.ts index 211906cd5..fcc86ec13 100644 --- a/apps/eclair/src/features/domain-map/queries/extract-domain-map.ts +++ b/apps/eclair/src/features/domain-map/queries/extract-domain-map.ts @@ -1,16 +1,12 @@ -import type { RiviereGraph } from '@living-architecture/riviere-schema' -import type { - Node, Edge -} from '@xyflow/react' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' +import type { Node, Edge } from '@xyflow/react' import dagre from 'dagre' import { getClosestHandle } from '@/platform/infra/layout/handle-positioning' -import { RiviereQuery } from '@living-architecture/riviere-query' +import { RiviereQuery } from '@living-architecture/riviere-builder-domain-model/query' import type * as DomainMapEdgeTypes from './edgeAggregation' import { LayoutError } from '@/platform/infra/errors/errors' import { aggregateDomainEdges } from './edge-aggregation' -import { - aggregateExternalEdges, createExternalNodeId -} from './external-domain-handling' +import { aggregateExternalEdges, createExternalNodeId } from './external-domain-handling' import { getEffectiveNodeType } from '@/platform/domain/node-type-presentation' import { semanticRelationshipSummary } from './edgeAggregation' diff --git a/apps/eclair/src/features/domains/components/DomainDetailView/DomainDetailView.spec.tsx b/apps/eclair/src/features/domains/components/DomainDetailView/DomainDetailView.spec.tsx index d0d4a6112..f6d5a8a65 100644 --- a/apps/eclair/src/features/domains/components/DomainDetailView/DomainDetailView.spec.tsx +++ b/apps/eclair/src/features/domains/components/DomainDetailView/DomainDetailView.spec.tsx @@ -6,7 +6,7 @@ import { extractDomainDetails } from '../../queries/extract-domain-details' import { parseNode, parseDomainMetadata, parseDomainKey } from '@/platform/infra/__fixtures__/riviere-test-fixtures' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import { assertDefined } from '@/test-assertions' const testSourceLocation = { repository: 'test-repo', diff --git a/apps/eclair/src/features/domains/components/DomainDetailView/DomainDetailView.tsx b/apps/eclair/src/features/domains/components/DomainDetailView/DomainDetailView.tsx index fbf06d55b..8fbed1c17 100644 --- a/apps/eclair/src/features/domains/components/DomainDetailView/DomainDetailView.tsx +++ b/apps/eclair/src/features/domains/components/DomainDetailView/DomainDetailView.tsx @@ -47,7 +47,7 @@ export function DomainDetailView({ return entitySearch === '' ? domain.entities : domain.entities.filter((entity) => - entity.name.toLowerCase().includes(entitySearch.toLowerCase()), + entity.name.value.toLowerCase().includes(entitySearch.toLowerCase()), ) }, [domain.entities, entitySearch]) @@ -376,7 +376,7 @@ function EntitiesListOrEmpty({ return (
{filteredEntities.map((entity) => ( - + ))}
) diff --git a/apps/eclair/src/features/domains/entrypoint/DomainDetailPage.spec.tsx b/apps/eclair/src/features/domains/entrypoint/DomainDetailPage.spec.tsx index e832ee344..76e75f13a 100644 --- a/apps/eclair/src/features/domains/entrypoint/DomainDetailPage.spec.tsx +++ b/apps/eclair/src/features/domains/entrypoint/DomainDetailPage.spec.tsx @@ -12,7 +12,7 @@ import { DomainDetailPage } from './DomainDetailPage' import { parseNode, parseEdge, parseDomainMetadata } from '@/platform/infra/__fixtures__/riviere-test-fixtures' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import { assertDefined } from '@/test-assertions' const testSourceLocation = { diff --git a/apps/eclair/src/features/domains/entrypoint/DomainDetailPage.tsx b/apps/eclair/src/features/domains/entrypoint/DomainDetailPage.tsx index 98741c8c1..e167e1de3 100644 --- a/apps/eclair/src/features/domains/entrypoint/DomainDetailPage.tsx +++ b/apps/eclair/src/features/domains/entrypoint/DomainDetailPage.tsx @@ -2,7 +2,7 @@ import { useState, useRef, useCallback, forwardRef } from 'react' import { useParams } from 'react-router-dom' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import { extractDomainDetails, type DomainDetails } from '../queries/extract-domain-details' diff --git a/apps/eclair/src/features/domains/queries/domain-node-breakdown.spec.ts b/apps/eclair/src/features/domains/queries/domain-node-breakdown.spec.ts index c0a876817..260007258 100644 --- a/apps/eclair/src/features/domains/queries/domain-node-breakdown.spec.ts +++ b/apps/eclair/src/features/domains/queries/domain-node-breakdown.spec.ts @@ -1,6 +1,4 @@ -import { - describe, it, expect -} from 'vitest' +import { describe, it, expect } from 'vitest' import { countNodesByType, formatDomainNodes, @@ -8,7 +6,7 @@ import { type NodeBreakdown, } from './domain-node-breakdown' import { parseNode } from '@/platform/infra/__fixtures__/riviere-test-fixtures' -import type { SourceLocation } from '@living-architecture/riviere-schema' +import type { SourceLocation } from '@living-architecture/riviere-schema-published-language/schema' import type { RawNode } from '@/platform/infra/__fixtures__/riviere-test-fixtures' const testSourceLocation = { diff --git a/apps/eclair/src/features/domains/queries/domain-node-breakdown.ts b/apps/eclair/src/features/domains/queries/domain-node-breakdown.ts index ac231deaf..b9f8435ab 100644 --- a/apps/eclair/src/features/domains/queries/domain-node-breakdown.ts +++ b/apps/eclair/src/features/domains/queries/domain-node-breakdown.ts @@ -1,4 +1,4 @@ -import type * as RiviereSchema from '@living-architecture/riviere-schema' +import type * as RiviereSchema from '@living-architecture/riviere-schema-published-language/schema' import * as EclairDomain from '@/platform/domain/eclair-types' import { getEffectiveNodeType } from '@/platform/domain/node-type-presentation' diff --git a/apps/eclair/src/features/domains/queries/extract-domain-details.advanced.spec.ts b/apps/eclair/src/features/domains/queries/extract-domain-details.advanced.spec.ts index 345a0897d..3776cd060 100644 --- a/apps/eclair/src/features/domains/queries/extract-domain-details.advanced.spec.ts +++ b/apps/eclair/src/features/domains/queries/extract-domain-details.advanced.spec.ts @@ -1,6 +1,4 @@ -import { - describe, it, expect -} from 'vitest' +import { describe, it, expect } from 'vitest' import { extractDomainDetails } from './extract-domain-details' import { parseNode, @@ -10,7 +8,7 @@ import { type RawNode, type RawEdge, } from '@/platform/infra/__fixtures__/riviere-test-fixtures' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' const testSourceLocation = { repository: 'test-repo', filePath: 'src/test.ts', @@ -571,7 +569,7 @@ describe('extractDomainDetails - advanced tests', () => { }) const result = extractDomainDetails(graph, parseDomainKey('order-domain')) - const orderEntity = result?.entities.find((e) => e.name === 'Order') + const orderEntity = result?.entities.find((e) => e.name.value === 'Order') const beginOp = orderEntity?.operations.find((op) => op.operationName === 'begin') expect(beginOp?.behavior).toStrictEqual({ @@ -611,7 +609,7 @@ describe('extractDomainDetails - advanced tests', () => { }) const result = extractDomainDetails(graph, parseDomainKey('order-domain')) - const orderEntity = result?.entities.find((e) => e.name === 'Order') + const orderEntity = result?.entities.find((e) => e.name.value === 'Order') const beginOp = orderEntity?.operations.find((op) => op.operationName === 'begin') expect(beginOp?.stateChanges).toStrictEqual([ @@ -658,7 +656,7 @@ describe('extractDomainDetails - advanced tests', () => { }) const result = extractDomainDetails(graph, parseDomainKey('order-domain')) - const orderEntity = result?.entities.find((e) => e.name === 'Order') + const orderEntity = result?.entities.find((e) => e.name.value === 'Order') const beginOp = orderEntity?.operations.find((op) => op.operationName === 'begin') expect(beginOp?.signature?.parameters).toHaveLength(2) @@ -693,7 +691,7 @@ describe('extractDomainDetails - advanced tests', () => { }) const result = extractDomainDetails(graph, parseDomainKey('order-domain')) - const orderEntity = result?.entities.find((e) => e.name === 'Order') + const orderEntity = result?.entities.find((e) => e.name.value === 'Order') const beginOp = orderEntity?.operations.find((op) => op.operationName === 'begin') expect(beginOp?.sourceLocation?.filePath).toBe('src/Order.ts') diff --git a/apps/eclair/src/features/domains/queries/extract-domain-details.entities.spec.ts b/apps/eclair/src/features/domains/queries/extract-domain-details.entities.spec.ts index 95a82047b..0eed937b5 100644 --- a/apps/eclair/src/features/domains/queries/extract-domain-details.entities.spec.ts +++ b/apps/eclair/src/features/domains/queries/extract-domain-details.entities.spec.ts @@ -1,6 +1,4 @@ -import { - describe, it, expect -} from 'vitest' +import { describe, it, expect } from 'vitest' import { extractDomainDetails } from './extract-domain-details' import { parseNode, @@ -8,7 +6,7 @@ import { parseDomainKey, type RawNode, } from '@/platform/infra/__fixtures__/riviere-test-fixtures' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' const testSourceLocation = { repository: 'test-repo', @@ -87,13 +85,13 @@ describe('extractDomainDetails entities extraction', () => { expect(result?.entities).toHaveLength(2) - const orderEntity = result?.entities.find((e) => e.name === 'Order') + const orderEntity = result?.entities.find((e) => e.name.value === 'Order') expect(orderEntity?.operations.map((op) => op.operationName)).toStrictEqual([ 'begin', 'confirm', ]) - const orderItemEntity = result?.entities.find((e) => e.name === 'OrderItem') + const orderItemEntity = result?.entities.find((e) => e.name.value === 'OrderItem') expect(orderItemEntity?.operations.map((op) => op.operationName)).toStrictEqual(['add']) }) @@ -137,7 +135,7 @@ describe('extractDomainDetails entities extraction', () => { const result = extractDomainDetails(graph, parseDomainKey('order-domain')) - expect(result?.entities.map((e) => e.name)).toStrictEqual(['Apple', 'Zebra']) + expect(result?.entities.map((e) => e.name.value)).toStrictEqual(['Apple', 'Zebra']) expect(result?.entities[0]?.operations.map((op) => op.operationName)).toStrictEqual([ 'aa', 'bb', @@ -210,7 +208,7 @@ describe('extractDomainDetails entities extraction', () => { }) const result = extractDomainDetails(graph, parseDomainKey('order-domain')) - const orderEntity = result?.entities.find((e) => e.name === 'Order') + const orderEntity = result?.entities.find((e) => e.name.value === 'Order') expect(orderEntity?.operations[0]?.sourceLocation).toStrictEqual({ repository: 'test-repo', @@ -246,7 +244,7 @@ describe('extractDomainDetails entities extraction', () => { }) const result = extractDomainDetails(graph, parseDomainKey('order-domain')) - const orderEntity = result?.entities.find((e) => e.name === 'Order') + const orderEntity = result?.entities.find((e) => e.name.value === 'Order') expect(orderEntity?.operations[0]?.sourceLocation).toStrictEqual({ repository: 'test-repo', @@ -278,7 +276,7 @@ describe('extractDomainDetails entities extraction', () => { const result = extractDomainDetails(graph, parseDomainKey('order-domain')) - const orderEntity = result?.entities.find((e) => e.name === 'Order') + const orderEntity = result?.entities.find((e) => e.name.value === 'Order') expect(orderEntity?.businessRules).toStrictEqual([]) }) }) diff --git a/apps/eclair/src/features/domains/queries/extract-domain-details.relationships.spec.ts b/apps/eclair/src/features/domains/queries/extract-domain-details.relationships.spec.ts index 2abe234d2..f2a884c87 100644 --- a/apps/eclair/src/features/domains/queries/extract-domain-details.relationships.spec.ts +++ b/apps/eclair/src/features/domains/queries/extract-domain-details.relationships.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import { parseDomainKey, parseDomainMetadata, diff --git a/apps/eclair/src/features/domains/queries/extract-domain-details.spec.ts b/apps/eclair/src/features/domains/queries/extract-domain-details.spec.ts index 6b512aebb..5d9364a36 100644 --- a/apps/eclair/src/features/domains/queries/extract-domain-details.spec.ts +++ b/apps/eclair/src/features/domains/queries/extract-domain-details.spec.ts @@ -1,6 +1,4 @@ -import { - describe, it, expect -} from 'vitest' +import { describe, it, expect } from 'vitest' import { extractDomainDetails } from './extract-domain-details' import { parseNode, @@ -8,7 +6,7 @@ import { parseDomainKey, type RawNode, } from '@/platform/infra/__fixtures__/riviere-test-fixtures' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' const testSourceLocation = { repository: 'test-repo', filePath: 'src/test.ts', diff --git a/apps/eclair/src/features/domains/queries/extract-domain-details.stateMachine.spec.ts b/apps/eclair/src/features/domains/queries/extract-domain-details.stateMachine.spec.ts index cb9f6e4c8..52b38e13b 100644 --- a/apps/eclair/src/features/domains/queries/extract-domain-details.stateMachine.spec.ts +++ b/apps/eclair/src/features/domains/queries/extract-domain-details.stateMachine.spec.ts @@ -1,6 +1,4 @@ -import { - describe, it, expect -} from 'vitest' +import { describe, it, expect } from 'vitest' import { extractDomainDetails } from './extract-domain-details' import { parseNode, @@ -8,7 +6,7 @@ import { parseDomainKey, type RawNode, } from '@/platform/infra/__fixtures__/riviere-test-fixtures' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' const testSourceLocation = { repository: 'test-repo', @@ -103,8 +101,13 @@ describe('extractDomainDetails - entity state machine', () => { }) const result = extractDomainDetails(graph, parseDomainKey('order-domain')) - const orderEntity = result?.entities.find((e) => e.name === 'Order') + const orderEntity = result?.entities.find((e) => e.name.value === 'Order') - expect(orderEntity?.states).toStrictEqual(['Draft', 'Placed', 'Confirmed', 'Shipped']) + expect(orderEntity?.states.map((state) => state.value)).toStrictEqual([ + 'Draft', + 'Placed', + 'Confirmed', + 'Shipped', + ]) }) }) diff --git a/apps/eclair/src/features/domains/queries/extract-domain-details.ts b/apps/eclair/src/features/domains/queries/extract-domain-details.ts index d19e01f42..4f3107018 100644 --- a/apps/eclair/src/features/domains/queries/extract-domain-details.ts +++ b/apps/eclair/src/features/domains/queries/extract-domain-details.ts @@ -1,4 +1,8 @@ -import type { RiviereGraph, SystemType, SourceLocation } from '@living-architecture/riviere-schema' +import type { + RiviereGraph, + SystemType, + SourceLocation, +} from '@living-architecture/riviere-schema-published-language/schema' import { nodeIdSchema, type DomainName, @@ -6,7 +10,8 @@ import { type EntryPoint, type NodeId, } from '@/platform/domain/eclair-types' -import { RiviereQuery, type Entity } from '@living-architecture/riviere-query' +import { RiviereQuery } from '@living-architecture/riviere-builder-domain-model/query' +import type { Entity } from '@living-architecture/riviere-builder-domain-model/query/entity' import { compareByCodePoint } from '@/platform/domain/compare-by-code-point' import type { NodeBreakdown, DomainNode } from './domain-node-breakdown' import { countNodesByType, formatDomainNodes, extractEntryPoints } from './domain-node-breakdown' @@ -131,33 +136,45 @@ export function extractDomainDetails( ) const publishedEvents: DomainEvent[] = queryPublished.map((pe) => { - const nodeId = nodeIdSchema.parse(pe.id) + const nodeId = nodeIdSchema.parse(pe.id.value) const component = componentById.get(nodeId) const schema = component?.type === 'Event' ? component.eventSchema : undefined return { - id: pe.id, - eventName: pe.eventName, + id: pe.id.value, + eventName: pe.eventName.value, sourceLocation: component?.sourceLocation, - handlers: pe.handlers, + handlers: pe.handlers.map((handler) => ({ + domain: handler.domain.value, + handlerId: handler.handlerId.value, + handlerName: handler.handlerName.value, + })), schema, } }) - const domainHandlers = queryHandlers.filter((h) => h.domain === domainId) + const domainHandlers = queryHandlers.filter((handler) => handler.domain.value === domainId) const consumedHandlers: DomainEventHandler[] = domainHandlers.map((h) => { - const nodeId = nodeIdSchema.parse(h.id) + const nodeId = nodeIdSchema.parse(h.id.value) const component = componentById.get(nodeId) const description = component?.description !== undefined && typeof component?.description === 'string' ? component.description : undefined return { - id: h.id, - handlerName: h.handlerName, + id: h.id.value, + handlerName: h.handlerName.value, description, sourceLocation: component?.sourceLocation, - subscribedEvents: h.subscribedEvents, - subscribedEventsWithDomain: h.subscribedEventsWithDomain, + subscribedEvents: h.subscribedEvents.map((eventName) => eventName.value), + subscribedEventsWithDomain: h.subscribedEventsWithDomain.map((event) => + event.sourceKnown + ? { + eventName: event.eventName.value, + sourceDomain: event.sourceDomain.value, + sourceKnown: true, + } + : { eventName: event.eventName.value, sourceKnown: false }, + ), } }) diff --git a/apps/eclair/src/features/empty-state/entrypoint/EmptyState.spec.tsx b/apps/eclair/src/features/empty-state/entrypoint/EmptyState.spec.tsx index f5c7013e3..d82a6a8c1 100644 --- a/apps/eclair/src/features/empty-state/entrypoint/EmptyState.spec.tsx +++ b/apps/eclair/src/features/empty-state/entrypoint/EmptyState.spec.tsx @@ -10,7 +10,7 @@ import { } from '@/platform/infra/__fixtures__/riviere-test-fixtures' import type { RiviereGraph, SourceLocation -} from '@living-architecture/riviere-schema' +} from '@living-architecture/riviere-schema-published-language/schema' import { dropFilesOnElement, getDropZone } from '@/test/setup' diff --git a/apps/eclair/src/features/empty-state/entrypoint/EmptyState.tsx b/apps/eclair/src/features/empty-state/entrypoint/EmptyState.tsx index ea5b3933c..254b2a5bd 100644 --- a/apps/eclair/src/features/empty-state/entrypoint/EmptyState.tsx +++ b/apps/eclair/src/features/empty-state/entrypoint/EmptyState.tsx @@ -1,7 +1,7 @@ import { useState } from 'react' import { FileUpload } from '@/platform/infra/file-upload/FileUpload' import { useGraph } from '@/platform/infra/graph-state/GraphContext' -import { parseRiviereGraph } from '@living-architecture/riviere-schema' +import { parseRiviereGraph } from '@living-architecture/riviere-schema-published-language/validation' export function EmptyState(): React.ReactElement { const { setGraph } = useGraph() @@ -11,8 +11,12 @@ export function EmptyState(): React.ReactElement { setError(null) try { const data: unknown = JSON.parse(content) - const graph = parseRiviereGraph(data) - setGraph(graph) + const result = parseRiviereGraph(data) + if (!result.success) { + setError(`Validation failed for ${fileName}:\n${result.issues.join('\n')}`) + return + } + setGraph(result.graph) } catch (e) { const message = e instanceof Error ? e.message : 'Unknown error' setError(`Validation failed for ${fileName}:\n${message}`) diff --git a/apps/eclair/src/features/entities/entrypoint/EntitiesPage.spec.tsx b/apps/eclair/src/features/entities/entrypoint/EntitiesPage.spec.tsx index 6904392df..73c73432a 100644 --- a/apps/eclair/src/features/entities/entrypoint/EntitiesPage.spec.tsx +++ b/apps/eclair/src/features/entities/entrypoint/EntitiesPage.spec.tsx @@ -10,7 +10,7 @@ import { EntitiesPage } from './EntitiesPage' import { parseNode, parseEdge, parseDomainMetadata } from '@/platform/infra/__fixtures__/riviere-test-fixtures' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' const testSourceLocation = { repository: 'test-repo', filePath: 'src/test.ts', diff --git a/apps/eclair/src/features/entities/entrypoint/EntitiesPage.tsx b/apps/eclair/src/features/entities/entrypoint/EntitiesPage.tsx index ebf0d364c..16aa3bbd1 100644 --- a/apps/eclair/src/features/entities/entrypoint/EntitiesPage.tsx +++ b/apps/eclair/src/features/entities/entrypoint/EntitiesPage.tsx @@ -2,9 +2,9 @@ import { useState, useMemo, useCallback } from 'react' import { useNavigate } from 'react-router-dom' -import { RiviereQuery } from '@living-architecture/riviere-query' -import type { Entity } from '@living-architecture/riviere-query' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import { RiviereQuery } from '@living-architecture/riviere-builder-domain-model/query' +import type { Entity } from '@living-architecture/riviere-builder-domain-model/query/entity' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import { EntityAccordion } from '@/platform/infra/ui/EntityAccordion/EntityAccordion' interface EntitiesPageProps {readonly graph: RiviereGraph} @@ -29,16 +29,16 @@ export function EntitiesPage({ graph }: Readonly): React.Reac const filteredEntities = useMemo(() => { return entities.filter((entity) => { const matchesSearch = - entity.name.toLowerCase().includes(searchQuery.toLowerCase()) || - entity.domain.toLowerCase().includes(searchQuery.toLowerCase()) - const matchesDomain = selectedDomain === 'all' || entity.domain === selectedDomain + entity.name.value.toLowerCase().includes(searchQuery.toLowerCase()) || + entity.domain.value.toLowerCase().includes(searchQuery.toLowerCase()) + const matchesDomain = selectedDomain === 'all' || entity.domain.value === selectedDomain return matchesSearch && matchesDomain }) }, [entities, searchQuery, selectedDomain]) const domains = useMemo(() => { - return Array.from(new Set(entities.map((e) => e.domain))) + return Array.from(new Set(entities.map((e) => e.domain.value))) }, [entities]) const totalOperations = useMemo(() => { @@ -120,7 +120,7 @@ export function EntitiesPage({ graph }: Readonly): React.Reac
{filteredEntities.map((entity) => ( diff --git a/apps/eclair/src/features/events/entrypoint/EventsPage.spec.tsx b/apps/eclair/src/features/events/entrypoint/EventsPage.spec.tsx index f2297a3bc..32d2e4c44 100644 --- a/apps/eclair/src/features/events/entrypoint/EventsPage.spec.tsx +++ b/apps/eclair/src/features/events/entrypoint/EventsPage.spec.tsx @@ -9,7 +9,7 @@ import { MemoryRouter } from 'react-router-dom' import { EventsPage } from './EventsPage' import type { RiviereGraph, SourceLocation -} from '@living-architecture/riviere-schema' +} from '@living-architecture/riviere-schema-published-language/schema' import { parseNode, parseEdge, parseDomainMetadata } from '@/platform/infra/__fixtures__/riviere-test-fixtures' diff --git a/apps/eclair/src/features/events/entrypoint/EventsPage.tsx b/apps/eclair/src/features/events/entrypoint/EventsPage.tsx index 284af3c2d..009c779ec 100644 --- a/apps/eclair/src/features/events/entrypoint/EventsPage.tsx +++ b/apps/eclair/src/features/events/entrypoint/EventsPage.tsx @@ -4,7 +4,7 @@ import { import { useNavigate, useSearchParams } from 'react-router-dom' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import { EventAccordion } from '@/platform/infra/ui/EventAccordion/EventAccordion' import { compareByCodePoint } from '../queries/compare-by-code-point' import type { DomainEvent } from '../queries/domain-event-types' diff --git a/apps/eclair/src/features/flows/components/FlowCard/FlowCard.spec.tsx b/apps/eclair/src/features/flows/components/FlowCard/FlowCard.spec.tsx index ce7547e84..e380830de 100644 --- a/apps/eclair/src/features/flows/components/FlowCard/FlowCard.spec.tsx +++ b/apps/eclair/src/features/flows/components/FlowCard/FlowCard.spec.tsx @@ -11,7 +11,7 @@ import { parseNode, parseEdge, parseDomainMetadata } from '@/platform/infra/__fixtures__/riviere-test-fixtures' import type { Flow } from '../../queries/extract-flows' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' const testSourceLocation = { repository: 'test-repo', filePath: 'src/test.ts', diff --git a/apps/eclair/src/features/flows/components/FlowCard/FlowCard.tsx b/apps/eclair/src/features/flows/components/FlowCard/FlowCard.tsx index e2dfdc013..4d9a088d3 100644 --- a/apps/eclair/src/features/flows/components/FlowCard/FlowCard.tsx +++ b/apps/eclair/src/features/flows/components/FlowCard/FlowCard.tsx @@ -1,5 +1,5 @@ import { useNavigate } from 'react-router-dom' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import type { Flow } from '../../queries/extract-flows' import { CodeLinkMenu } from '@/platform/infra/ui/CodeLinkMenu/CodeLinkMenu' import { FlowTrace } from '../FlowTrace/FlowTrace' diff --git a/apps/eclair/src/features/flows/components/FlowTrace/FlowGraphView.spec.tsx b/apps/eclair/src/features/flows/components/FlowTrace/FlowGraphView.spec.tsx index df85718a4..8b58ec342 100644 --- a/apps/eclair/src/features/flows/components/FlowTrace/FlowGraphView.spec.tsx +++ b/apps/eclair/src/features/flows/components/FlowTrace/FlowGraphView.spec.tsx @@ -9,7 +9,7 @@ import { parseNode, parseEdge, parseDomainMetadata } from '@/platform/infra/__fixtures__/riviere-test-fixtures' import type { FlowStep } from '../../queries/extract-flows' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import type { TooltipData } from '@/platform/infra/graph/graph-types' const testSourceLocation = { diff --git a/apps/eclair/src/features/flows/components/FlowTrace/FlowGraphView.tsx b/apps/eclair/src/features/flows/components/FlowTrace/FlowGraphView.tsx index 9da527b95..7a955248a 100644 --- a/apps/eclair/src/features/flows/components/FlowTrace/FlowGraphView.tsx +++ b/apps/eclair/src/features/flows/components/FlowTrace/FlowGraphView.tsx @@ -5,7 +5,7 @@ import { ForceGraph } from '@/platform/infra/graph/ForceGraph/ForceGraph' import { GraphTooltip } from '@/platform/infra/graph/GraphTooltip/GraphTooltip' import type { TooltipData } from '@/platform/infra/graph/graph-types' import type { FlowStep } from '../../queries/extract-flows' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import type { Theme } from '@/types/theme' import { DEFAULT_THEME } from '@/types/theme' diff --git a/apps/eclair/src/features/flows/components/FlowTrace/FlowTrace.spec.tsx b/apps/eclair/src/features/flows/components/FlowTrace/FlowTrace.spec.tsx index 9e7e6e69f..f9c849d45 100644 --- a/apps/eclair/src/features/flows/components/FlowTrace/FlowTrace.spec.tsx +++ b/apps/eclair/src/features/flows/components/FlowTrace/FlowTrace.spec.tsx @@ -7,7 +7,7 @@ import { import userEvent from '@testing-library/user-event' import { FlowTrace } from './FlowTrace' import type { FlowStep } from '../../queries/extract-flows' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import { parseNode, parseEdge, parseDomainMetadata } from '@/platform/infra/__fixtures__/riviere-test-fixtures' diff --git a/apps/eclair/src/features/flows/components/FlowTrace/FlowTrace.tsx b/apps/eclair/src/features/flows/components/FlowTrace/FlowTrace.tsx index 7fd0b0f6b..560af180c 100644 --- a/apps/eclair/src/features/flows/components/FlowTrace/FlowTrace.tsx +++ b/apps/eclair/src/features/flows/components/FlowTrace/FlowTrace.tsx @@ -1,8 +1,7 @@ import { useState } from 'react' import type { FlowStep } from '../../queries/extract-flows' -import { - createLinkId, type RiviereGraph -} from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' +import { LinkId } from '@living-architecture/riviere-schema-published-language/link-id' import { FlowGraphView } from './FlowGraphView' import { getNodeTypeColor } from '@/platform/domain/node-type-presentation' import type { Theme } from '@/types/theme' @@ -107,7 +106,7 @@ export function FlowTrace({ {(step.outgoingLinks?.length ?? 0) > 0 && (
{step.outgoingLinks?.map((link) => ( -
+
{relationshipDetail(link)} → {componentNames.get(link.target) ?? link.target}
))} diff --git a/apps/eclair/src/features/flows/entrypoint/FlowsPage.spec.tsx b/apps/eclair/src/features/flows/entrypoint/FlowsPage.spec.tsx index 8ff038837..0cc56c7ce 100644 --- a/apps/eclair/src/features/flows/entrypoint/FlowsPage.spec.tsx +++ b/apps/eclair/src/features/flows/entrypoint/FlowsPage.spec.tsx @@ -7,7 +7,7 @@ import { import { MemoryRouter } from 'react-router-dom' import userEvent from '@testing-library/user-event' import { FlowsPage } from './FlowsPage' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import { parseNode, parseEdge, parseDomainMetadata } from '@/platform/infra/__fixtures__/riviere-test-fixtures' diff --git a/apps/eclair/src/features/flows/entrypoint/FlowsPage.tsx b/apps/eclair/src/features/flows/entrypoint/FlowsPage.tsx index 413f2b03f..9f76706eb 100644 --- a/apps/eclair/src/features/flows/entrypoint/FlowsPage.tsx +++ b/apps/eclair/src/features/flows/entrypoint/FlowsPage.tsx @@ -1,5 +1,5 @@ import { useMemo } from 'react' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import { compareByCodePoint } from '../queries/compare-by-code-point' import { extractFlows } from '../queries/extract-flows' import { FlowCard } from '../components/FlowCard/FlowCard' diff --git a/apps/eclair/src/features/flows/queries/extract-flows.spec.ts b/apps/eclair/src/features/flows/queries/extract-flows.spec.ts index 3a5207b8e..17a8b7e1e 100644 --- a/apps/eclair/src/features/flows/queries/extract-flows.spec.ts +++ b/apps/eclair/src/features/flows/queries/extract-flows.spec.ts @@ -1,8 +1,6 @@ -import { - describe, it, expect -} from 'vitest' +import { describe, it, expect } from 'vitest' import { extractFlows } from './extract-flows' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import { parseNode, parseEdge, diff --git a/apps/eclair/src/features/flows/queries/extract-flows.ts b/apps/eclair/src/features/flows/queries/extract-flows.ts index a90d8b301..92b58f320 100644 --- a/apps/eclair/src/features/flows/queries/extract-flows.ts +++ b/apps/eclair/src/features/flows/queries/extract-flows.ts @@ -1,15 +1,13 @@ -import { - RiviereQuery, - type Flow as QueryFlow, - type FlowStep as QueryFlowStep, -} from '@living-architecture/riviere-query' +import { RiviereQuery } from '@living-architecture/riviere-builder-domain-model/query' +import type { Flow as QueryFlow } from '@living-architecture/riviere-builder-domain-model/query/flow' +import type { FlowStep as QueryFlowStep } from '@living-architecture/riviere-builder-domain-model/query/flow-step' import type { Component, ExternalLink, Link, RiviereGraph, SourceLocation, -} from '@living-architecture/riviere-schema' +} from '@living-architecture/riviere-schema-published-language/schema' import { getEffectiveNodeType } from '@/platform/domain/node-type-presentation' export interface EntryPoint { diff --git a/apps/eclair/src/features/full-graph/entrypoint/FullGraphPage.spec.tsx b/apps/eclair/src/features/full-graph/entrypoint/FullGraphPage.spec.tsx index d049ae754..fb53387d2 100644 --- a/apps/eclair/src/features/full-graph/entrypoint/FullGraphPage.spec.tsx +++ b/apps/eclair/src/features/full-graph/entrypoint/FullGraphPage.spec.tsx @@ -8,7 +8,7 @@ import { userEvent } from '@testing-library/user-event' import { MemoryRouter } from 'react-router-dom' import { FullGraphPage } from './FullGraphPage' import { ExportProvider } from '@/platform/infra/export/ExportContext' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import { parseNode, parseEdge, parseDomainKey } from '@/platform/infra/__fixtures__/riviere-test-fixtures' diff --git a/apps/eclair/src/features/full-graph/entrypoint/FullGraphPage.tsx b/apps/eclair/src/features/full-graph/entrypoint/FullGraphPage.tsx index 7c53245f4..f633c1da7 100644 --- a/apps/eclair/src/features/full-graph/entrypoint/FullGraphPage.tsx +++ b/apps/eclair/src/features/full-graph/entrypoint/FullGraphPage.tsx @@ -2,7 +2,7 @@ import { useState, useCallback, useMemo, useRef, useEffect } from 'react' import { useSearchParams } from 'react-router-dom' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import type { Node, Edge, } from '../queries/eclair-types' diff --git a/apps/eclair/src/features/full-graph/queries/extract-node-types.ts b/apps/eclair/src/features/full-graph/queries/extract-node-types.ts index 4db2c24d7..821ea2f2d 100644 --- a/apps/eclair/src/features/full-graph/queries/extract-node-types.ts +++ b/apps/eclair/src/features/full-graph/queries/extract-node-types.ts @@ -1,4 +1,4 @@ -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import { getEffectiveNodeType } from '@/platform/domain/node-type-presentation' import { compareByCodePoint } from '@/platform/domain/compare-by-code-point' diff --git a/apps/eclair/src/features/modules/entrypoint/ModulesPage.spec.tsx b/apps/eclair/src/features/modules/entrypoint/ModulesPage.spec.tsx index f26f6147e..9001d34cc 100644 --- a/apps/eclair/src/features/modules/entrypoint/ModulesPage.spec.tsx +++ b/apps/eclair/src/features/modules/entrypoint/ModulesPage.spec.tsx @@ -5,7 +5,7 @@ import userEvent from '@testing-library/user-event' import { describe, expect, it, } from 'vitest' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import { ThemeProvider } from '@/platform/infra/theme/ThemeContext' import { ModulesPage } from './ModulesPage' diff --git a/apps/eclair/src/features/modules/entrypoint/ModulesPage.tsx b/apps/eclair/src/features/modules/entrypoint/ModulesPage.tsx index 630c002f4..6e0394e78 100644 --- a/apps/eclair/src/features/modules/entrypoint/ModulesPage.tsx +++ b/apps/eclair/src/features/modules/entrypoint/ModulesPage.tsx @@ -1,5 +1,5 @@ import { useMemo } from 'react' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import { NodeTypeBadge } from '@/platform/infra/ui/NodeTypeBadge/NodeTypeBadge' import { extractModules } from '../queries/extract-modules' import type { Theme } from '@/types/theme' diff --git a/apps/eclair/src/features/modules/queries/extract-modules.spec.ts b/apps/eclair/src/features/modules/queries/extract-modules.spec.ts index 1cc537c76..672e679e7 100644 --- a/apps/eclair/src/features/modules/queries/extract-modules.spec.ts +++ b/apps/eclair/src/features/modules/queries/extract-modules.spec.ts @@ -4,7 +4,7 @@ import { expect, it, } from 'vitest' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import { extractModules } from './extract-modules' const graph: RiviereGraph = { diff --git a/apps/eclair/src/features/modules/queries/extract-modules.ts b/apps/eclair/src/features/modules/queries/extract-modules.ts index 278eeec8d..66a1c7ee2 100644 --- a/apps/eclair/src/features/modules/queries/extract-modules.ts +++ b/apps/eclair/src/features/modules/queries/extract-modules.ts @@ -1,4 +1,4 @@ -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import { getEffectiveNodeType, getNodeTypeDescription, diff --git a/apps/eclair/src/features/overview/entrypoint/OverviewPage.displayLimits.spec.tsx b/apps/eclair/src/features/overview/entrypoint/OverviewPage.displayLimits.spec.tsx index 740c281f6..13bb2db41 100644 --- a/apps/eclair/src/features/overview/entrypoint/OverviewPage.displayLimits.spec.tsx +++ b/apps/eclair/src/features/overview/entrypoint/OverviewPage.displayLimits.spec.tsx @@ -6,7 +6,7 @@ import { } from '@testing-library/react' import { MemoryRouter } from 'react-router-dom' import { OverviewPage } from './OverviewPage' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import { parseNode, parseDomainMetadata } from '@/platform/infra/__fixtures__/riviere-test-fixtures' diff --git a/apps/eclair/src/features/overview/entrypoint/OverviewPage.spec.tsx b/apps/eclair/src/features/overview/entrypoint/OverviewPage.spec.tsx index 7e8f237a0..700bb25af 100644 --- a/apps/eclair/src/features/overview/entrypoint/OverviewPage.spec.tsx +++ b/apps/eclair/src/features/overview/entrypoint/OverviewPage.spec.tsx @@ -7,7 +7,7 @@ import { import userEvent from '@testing-library/user-event' import { MemoryRouter } from 'react-router-dom' import { OverviewPage } from './OverviewPage' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import { parseNode, parseEdge, parseDomainMetadata } from '@/platform/infra/__fixtures__/riviere-test-fixtures' diff --git a/apps/eclair/src/features/overview/entrypoint/OverviewPage.tsx b/apps/eclair/src/features/overview/entrypoint/OverviewPage.tsx index fd1e22cfb..cd3560bee 100644 --- a/apps/eclair/src/features/overview/entrypoint/OverviewPage.tsx +++ b/apps/eclair/src/features/overview/entrypoint/OverviewPage.tsx @@ -4,7 +4,7 @@ import { import { Link } from 'react-router-dom' import type { RiviereGraph, SystemType -} from '@living-architecture/riviere-schema' +} from '@living-architecture/riviere-schema-published-language/schema' import { domainNameSchema, type DomainName, } from '../queries/eclair-domain' @@ -94,7 +94,9 @@ export function OverviewPage({ const repository = domainComponents.find((node) => node.sourceLocation != null) ?.sourceLocation?.repository - const entities = allEntities.filter((e) => e.domain === domain.name).map((e) => e.name) + const entities = allEntities + .filter((entity) => entity.domain.value === domain.name) + .map((entity) => entity.name.value) return { id: domainId, diff --git a/apps/eclair/src/features/overview/queries/node-type-breakdown.ts b/apps/eclair/src/features/overview/queries/node-type-breakdown.ts index b142d0a0d..904e5d667 100644 --- a/apps/eclair/src/features/overview/queries/node-type-breakdown.ts +++ b/apps/eclair/src/features/overview/queries/node-type-breakdown.ts @@ -1,4 +1,4 @@ -import type { Component } from '@living-architecture/riviere-schema' +import type { Component } from '@living-architecture/riviere-schema-published-language/schema' import { getEffectiveNodeType } from '@/platform/domain/node-type-presentation' export type NodeTypeBreakdown = Record diff --git a/apps/eclair/src/platform/domain/compare-by-code-point.ts b/apps/eclair/src/platform/domain/compare-by-code-point.ts index dfd4da3da..1995c8a9d 100644 --- a/apps/eclair/src/platform/domain/compare-by-code-point.ts +++ b/apps/eclair/src/platform/domain/compare-by-code-point.ts @@ -1 +1 @@ -export { compareByCodePoint } from '@living-architecture/riviere-query' +export { compareByCodePoint } from '@living-architecture/riviere-builder-domain-model/query/compare-by-code-point' diff --git a/apps/eclair/src/platform/domain/domain-event-types.ts b/apps/eclair/src/platform/domain/domain-event-types.ts index efa879c7a..fd3d50875 100644 --- a/apps/eclair/src/platform/domain/domain-event-types.ts +++ b/apps/eclair/src/platform/domain/domain-event-types.ts @@ -1,6 +1,7 @@ -import type { SourceLocation } from '@living-architecture/riviere-schema' +import type { SourceLocation } from '@living-architecture/riviere-schema-published-language/schema' export interface EventSubscriber { + handlerId: string domain: string handlerName: string } diff --git a/apps/eclair/src/platform/domain/domain-node-types.ts b/apps/eclair/src/platform/domain/domain-node-types.ts index 6b8c900d1..183bc0b28 100644 --- a/apps/eclair/src/platform/domain/domain-node-types.ts +++ b/apps/eclair/src/platform/domain/domain-node-types.ts @@ -1,4 +1,4 @@ -import type { SystemType } from '@living-architecture/riviere-schema' +import type { SystemType } from '@living-architecture/riviere-schema-published-language/schema' export type DomainMapSystemType = SystemType | 'external' diff --git a/apps/eclair/src/platform/domain/eclair-types.ts b/apps/eclair/src/platform/domain/eclair-types.ts index 0597417e0..7755b27df 100644 --- a/apps/eclair/src/platform/domain/eclair-types.ts +++ b/apps/eclair/src/platform/domain/eclair-types.ts @@ -5,7 +5,7 @@ import type { Link, Component, APIComponent, -} from '@living-architecture/riviere-schema' +} from '@living-architecture/riviere-schema-published-language/schema' export type NodeType = ComponentType | 'External' diff --git a/apps/eclair/src/platform/domain/graph-stats/compute-graph-stats.spec.ts b/apps/eclair/src/platform/domain/graph-stats/compute-graph-stats.spec.ts index 5150ed297..6a7b868bd 100644 --- a/apps/eclair/src/platform/domain/graph-stats/compute-graph-stats.spec.ts +++ b/apps/eclair/src/platform/domain/graph-stats/compute-graph-stats.spec.ts @@ -1,10 +1,9 @@ -import { - describe, it, expect -} from 'vitest' +import { describe, it, expect } from 'vitest' import { computeGraphStats } from './compute-graph-stats' import type { - RiviereGraph, SourceLocation -} from '@living-architecture/riviere-schema' + RiviereGraph, + SourceLocation, +} from '@living-architecture/riviere-schema-published-language/schema' import { parseNode, parseEdge, diff --git a/apps/eclair/src/platform/domain/graph-stats/compute-graph-stats.ts b/apps/eclair/src/platform/domain/graph-stats/compute-graph-stats.ts index b0bdd9d42..8331e7f73 100644 --- a/apps/eclair/src/platform/domain/graph-stats/compute-graph-stats.ts +++ b/apps/eclair/src/platform/domain/graph-stats/compute-graph-stats.ts @@ -1,4 +1,4 @@ -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' export interface GraphStats { totalNodes: number diff --git a/apps/eclair/src/platform/domain/node-type-presentation.spec.ts b/apps/eclair/src/platform/domain/node-type-presentation.spec.ts index 02d26d3d2..1e72159a4 100644 --- a/apps/eclair/src/platform/domain/node-type-presentation.spec.ts +++ b/apps/eclair/src/platform/domain/node-type-presentation.spec.ts @@ -4,7 +4,7 @@ import { expect, it, } from 'vitest' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import { getEffectiveNodeType, getNodeTypeColor, diff --git a/apps/eclair/src/platform/domain/node-type-presentation.ts b/apps/eclair/src/platform/domain/node-type-presentation.ts index ab7fdccff..da5cc9376 100644 --- a/apps/eclair/src/platform/domain/node-type-presentation.ts +++ b/apps/eclair/src/platform/domain/node-type-presentation.ts @@ -2,7 +2,7 @@ import type { Component, RiviereGraph, CustomTypeDefinition, -} from '@living-architecture/riviere-schema' +} from '@living-architecture/riviere-schema-published-language/schema' import type { Theme } from '@/types/theme' import { compareByCodePoint } from './compare-by-code-point' diff --git a/apps/eclair/src/platform/infra/__fixtures__/riviere-test-fixtures.ts b/apps/eclair/src/platform/infra/__fixtures__/riviere-test-fixtures.ts index ae5c57885..378dcffc1 100644 --- a/apps/eclair/src/platform/infra/__fixtures__/riviere-test-fixtures.ts +++ b/apps/eclair/src/platform/infra/__fixtures__/riviere-test-fixtures.ts @@ -6,7 +6,7 @@ import type { OperationBehavior, DomainMetadata, SystemType, -} from '@living-architecture/riviere-schema' +} from '@living-architecture/riviere-schema-published-language/schema' import { nodeIdSchema, edgeIdSchema, @@ -283,13 +283,13 @@ export function parseDomainMetadata( const entities: | Record, EntityDefinition> | undefined = value.entities - ? Object.fromEntries( + ? Object.fromEntries( Object.entries(value.entities).map(([entityName, definition]) => [ entityNameSchema.parse(entityName), definition, ]), ) - : undefined + : undefined const parsedValue: DomainMetadata = { description: value.description, diff --git a/apps/eclair/src/platform/infra/graph-state/GraphContext.spec.tsx b/apps/eclair/src/platform/infra/graph-state/GraphContext.spec.tsx index 253e215fa..575971a89 100644 --- a/apps/eclair/src/platform/infra/graph-state/GraphContext.spec.tsx +++ b/apps/eclair/src/platform/infra/graph-state/GraphContext.spec.tsx @@ -11,7 +11,7 @@ import { fetchAndValidateDemoGraph, buildDemoGraphUrl, } from './GraphContext' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import { parseNode, parseDomainKey } from '@/platform/infra/__fixtures__/riviere-test-fixtures' diff --git a/apps/eclair/src/platform/infra/graph-state/GraphContext.tsx b/apps/eclair/src/platform/infra/graph-state/GraphContext.tsx index b3c09a788..c57a1de0d 100644 --- a/apps/eclair/src/platform/infra/graph-state/GraphContext.tsx +++ b/apps/eclair/src/platform/infra/graph-state/GraphContext.tsx @@ -8,9 +8,8 @@ import { useSyncExternalStore, useMemo, } from 'react' -import { - parseRiviereGraph, type RiviereGraph -} from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' +import { parseRiviereGraph } from '@living-architecture/riviere-schema-published-language/validation' import { graphNameSchema, type GraphName } from '@/platform/domain/eclair-types' @@ -45,7 +44,11 @@ export async function fetchAndValidateDemoGraph( } const content = await response.text() const data: unknown = JSON.parse(content) - return parseRiviereGraph(data) + const result = parseRiviereGraph(data) + if (!result.success) { + throw new GraphError(`Invalid RiviereGraph:\n${result.issues.join('\n')}`) + } + return result.graph } function getIsDemoMode(): boolean { diff --git a/apps/eclair/src/platform/infra/graph/ForceGraph/ForceGraph.focus-lifecycle.spec.tsx b/apps/eclair/src/platform/infra/graph/ForceGraph/ForceGraph.focus-lifecycle.spec.tsx index 0bb6d6d58..8facee1d2 100644 --- a/apps/eclair/src/platform/infra/graph/ForceGraph/ForceGraph.focus-lifecycle.spec.tsx +++ b/apps/eclair/src/platform/infra/graph/ForceGraph/ForceGraph.focus-lifecycle.spec.tsx @@ -4,7 +4,7 @@ import { import { act, render, waitFor, } from '@testing-library/react' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import { ForceGraph } from './ForceGraph' import { parseDomainMetadata, parseNode, diff --git a/apps/eclair/src/platform/infra/graph/ForceGraph/ForceGraph.spec.tsx b/apps/eclair/src/platform/infra/graph/ForceGraph/ForceGraph.spec.tsx index 30809edad..4968de771 100644 --- a/apps/eclair/src/platform/infra/graph/ForceGraph/ForceGraph.spec.tsx +++ b/apps/eclair/src/platform/infra/graph/ForceGraph/ForceGraph.spec.tsx @@ -5,7 +5,7 @@ import { render, screen } from '@testing-library/react' import { ForceGraph } from './ForceGraph' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import type { Theme } from '@/types/theme' import { parseNode, diff --git a/apps/eclair/src/platform/infra/graph/ForceGraph/ForceGraph.tsx b/apps/eclair/src/platform/infra/graph/ForceGraph/ForceGraph.tsx index dbdf43f53..22572a4b4 100644 --- a/apps/eclair/src/platform/infra/graph/ForceGraph/ForceGraph.tsx +++ b/apps/eclair/src/platform/infra/graph/ForceGraph/ForceGraph.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useCallback, useState, useMemo } from 'react' import * as d3 from 'd3' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import type { Edge } from '@/platform/domain/eclair-types' import { compareByCodePoint } from '@/platform/domain/compare-by-code-point' import type { Theme } from '@/types/theme' diff --git a/apps/eclair/src/platform/infra/graph/ForceGraph/VisualizationDataAdapters.spec.ts b/apps/eclair/src/platform/infra/graph/ForceGraph/VisualizationDataAdapters.spec.ts index 1fa7c2e93..b68af0dfd 100644 --- a/apps/eclair/src/platform/infra/graph/ForceGraph/VisualizationDataAdapters.spec.ts +++ b/apps/eclair/src/platform/infra/graph/ForceGraph/VisualizationDataAdapters.spec.ts @@ -1,6 +1,4 @@ -import { - describe, it, expect -} from 'vitest' +import { describe, it, expect } from 'vitest' import { createSimulationNodes, createSimulationLinks, @@ -16,10 +14,8 @@ import { getSemanticEdgeColor, getDomainColor, } from './VisualizationDataAdapters' -import type { ExternalLink } from '@living-architecture/riviere-schema' -import type { - Node, Edge -} from '@/platform/domain/eclair-types' +import type { ExternalLink } from '@living-architecture/riviere-schema-published-language/schema' +import type { Node, Edge } from '@/platform/domain/eclair-types' import { parseNode, parseEdge, @@ -69,7 +65,9 @@ describe('VisualizationDataAdapters', () => { }), ] - const result = createSimulationNodes(nodes, {Job: { description: 'A scheduled unit of work' },}) + const result = createSimulationNodes(nodes, { + Job: { description: 'A scheduled unit of work' }, + }) expect(result[0]).toMatchObject({ type: 'Custom', diff --git a/apps/eclair/src/platform/infra/graph/ForceGraph/VisualizationDataAdapters.ts b/apps/eclair/src/platform/infra/graph/ForceGraph/VisualizationDataAdapters.ts index 6364866e8..cae2e72b7 100644 --- a/apps/eclair/src/platform/infra/graph/ForceGraph/VisualizationDataAdapters.ts +++ b/apps/eclair/src/platform/infra/graph/ForceGraph/VisualizationDataAdapters.ts @@ -1,7 +1,5 @@ -import type * as RiviereSchema from '@living-architecture/riviere-schema' -import type { - Node, NodeType, Edge -} from '@/platform/domain/eclair-types' +import type * as RiviereSchema from '@living-architecture/riviere-schema-published-language/schema' +import type { Node, NodeType, Edge } from '@/platform/domain/eclair-types' import * as GraphTypes from '../graph-types' import type { Theme } from '@/types/theme' import { diff --git a/apps/eclair/src/platform/infra/riviere-query/useRiviereQuery.ts b/apps/eclair/src/platform/infra/riviere-query/useRiviereQuery.ts index d5b290140..5b2930c34 100644 --- a/apps/eclair/src/platform/infra/riviere-query/useRiviereQuery.ts +++ b/apps/eclair/src/platform/infra/riviere-query/useRiviereQuery.ts @@ -1,6 +1,6 @@ import { useMemo } from 'react' -import { RiviereQuery } from '@living-architecture/riviere-query' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import { RiviereQuery } from '@living-architecture/riviere-builder-domain-model/query' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' export function useRiviereQuery(graph: RiviereGraph | null): RiviereQuery | null { return useMemo(() => { diff --git a/apps/eclair/src/platform/infra/ui/EntityAccordion/EntityAccordion.spec.tsx b/apps/eclair/src/platform/infra/ui/EntityAccordion/EntityAccordion.spec.tsx index f1ec81847..82729591a 100644 --- a/apps/eclair/src/platform/infra/ui/EntityAccordion/EntityAccordion.spec.tsx +++ b/apps/eclair/src/platform/infra/ui/EntityAccordion/EntityAccordion.spec.tsx @@ -6,10 +6,11 @@ import { } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { EntityAccordion } from './EntityAccordion' -import { Entity } from '@living-architecture/riviere-query' +import { RiviereQuery } from '@living-architecture/riviere-builder-domain-model/query' +import type { Entity } from '@living-architecture/riviere-builder-domain-model/query/entity' import type { DomainOpComponent, SourceLocation -} from '@living-architecture/riviere-schema' +} from '@living-architecture/riviere-schema-published-language/schema' const defaultSourceLocation: SourceLocation = { repository: 'test-repo', @@ -66,18 +67,55 @@ function createEntity( from: 'Pending', to: 'Confirmed', }, + { + from: 'Confirmed', + to: 'Cancelled', + }, ], }), ] - return new Entity( - overrides.name ?? 'Order', - overrides.domain ?? 'orders', - operations, - overrides.states ?? ['Draft', 'Pending', 'Confirmed', 'Cancelled'], - [], - overrides.businessRules ?? [], - ) + const requestedStates = overrides.states + const operationsWithRequestedStates = + requestedStates === undefined || operations.length === 0 + ? operations + : operations.map((operation, index) => + index === 0 + ? { + ...operation, + stateChanges: requestedStates.slice(1).map((state, stateIndex) => ({ + from: requestedStates[stateIndex] ?? '*', + to: state, + })), + } + : { ...operation, stateChanges: undefined }, + ) + + const domain = overrides.domain ?? 'orders' + const entityName = overrides.name ?? 'Order' + const query = new RiviereQuery({ + version: '1.0', + metadata: { + name: 'Entity accordion fixture', + description: 'Entity accordion fixture', + domains: { + [domain]: { + description: `${domain} domain`, + systemType: 'domain', + }, + }, + }, + components: operationsWithRequestedStates.map((operation) => ({ + ...operation, + domain, + entity: entityName, + name: `${entityName}.${operation.operationName}`, + })), + links: [], + }) + const entity = query.entities()[0] + if (entity === undefined) expect.fail('Entity accordion fixture must contain an entity') + return entity } describe('EntityAccordion', () => { @@ -93,15 +131,15 @@ describe('EntityAccordion', () => { operations: [ createDomainOp({ id: 'op-a', - operationName: 'a', + operationName: 'aa', }), createDomainOp({ id: 'op-b', - operationName: 'b', + operationName: 'bb', }), createDomainOp({ id: 'op-c', - operationName: 'c', + operationName: 'cc', }), ], }) diff --git a/apps/eclair/src/platform/infra/ui/EntityAccordion/EntityAccordion.tsx b/apps/eclair/src/platform/infra/ui/EntityAccordion/EntityAccordion.tsx index f8876c9f8..297ae28bd 100644 --- a/apps/eclair/src/platform/infra/ui/EntityAccordion/EntityAccordion.tsx +++ b/apps/eclair/src/platform/infra/ui/EntityAccordion/EntityAccordion.tsx @@ -1,6 +1,6 @@ import { useState } from 'react' -import type { Entity } from '@living-architecture/riviere-query' -import type { DomainOpComponent } from '@living-architecture/riviere-schema' +import type { Entity } from '@living-architecture/riviere-builder-domain-model/query/entity' +import type { DomainOpComponent } from '@living-architecture/riviere-schema-published-language/schema' import { CodeLinkMenu } from '@/platform/infra/ui/CodeLinkMenu/CodeLinkMenu' import { MethodCardChevron } from './MethodCardChevron' @@ -84,7 +84,7 @@ export function EntityAccordion({
- {entity.name} + {entity.name.value} {operationCount} operation{operationCount === 1 ? '' : 's'} @@ -118,11 +118,11 @@ export function EntityAccordion({ } const borderClass = getStateBorderClass() return ( -
+
- {state} + {state.value} {index < entity.states.length - 1 && ( diff --git a/apps/eclair/src/shell/App.spec.tsx b/apps/eclair/src/shell/App.spec.tsx index 3e7118fe2..fcc2dccef 100644 --- a/apps/eclair/src/shell/App.spec.tsx +++ b/apps/eclair/src/shell/App.spec.tsx @@ -16,7 +16,7 @@ import { } from '@/platform/infra/graph-state/GraphContext' import { ExportProvider } from '@/platform/infra/export/ExportContext' import { ThemeProvider } from '@/platform/infra/theme/ThemeContext' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import { parseNode, parseDomainMetadata } from '@/platform/infra/__fixtures__/riviere-test-fixtures' diff --git a/apps/eclair/src/shell/App.tsx b/apps/eclair/src/shell/App.tsx index 719848ae6..3776d62e6 100644 --- a/apps/eclair/src/shell/App.tsx +++ b/apps/eclair/src/shell/App.tsx @@ -7,7 +7,7 @@ import { } from '@/platform/infra/graph-state/GraphContext' import { ExportProvider } from '@/platform/infra/export/ExportContext' import { EmptyState } from '@/features/empty-state/entrypoint/EmptyState' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import { OverviewPage } from '@/features/overview/entrypoint/OverviewPage' import { FullGraphPage } from '@/features/full-graph/entrypoint/FullGraphPage' import { DomainMapPage } from '@/features/domain-map/entrypoint/DomainMapPage' diff --git a/apps/eclair/src/shell/components/AppShell/AppShell.spec.tsx b/apps/eclair/src/shell/components/AppShell/AppShell.spec.tsx index 08dea8889..8561514ee 100644 --- a/apps/eclair/src/shell/components/AppShell/AppShell.spec.tsx +++ b/apps/eclair/src/shell/components/AppShell/AppShell.spec.tsx @@ -9,7 +9,7 @@ import { } from 'vitest' import { AppShell } from './AppShell' import { ExportProvider } from '@/platform/infra/export/ExportContext' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import { graphNameSchema, nodeIdSchema, domainNameSchema, moduleNameSchema, type GraphName } from '@/platform/domain/eclair-types' diff --git a/apps/eclair/src/shell/components/AppShell/AppShell.tsx b/apps/eclair/src/shell/components/AppShell/AppShell.tsx index b7b318703..774f65dd6 100644 --- a/apps/eclair/src/shell/components/AppShell/AppShell.tsx +++ b/apps/eclair/src/shell/components/AppShell/AppShell.tsx @@ -5,7 +5,7 @@ import { useLocation } from 'react-router-dom' import { Header } from '@/shell/components/Header/Header' import { Sidebar } from '@/shell/components/Sidebar/Sidebar' import { useExport } from '@/platform/infra/export/ExportContext' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import type { GraphName } from '@/platform/domain/eclair-types' interface AppShellProps { diff --git a/apps/eclair/src/shell/components/Header/Header.spec.tsx b/apps/eclair/src/shell/components/Header/Header.spec.tsx index f9c1ec7cd..dc459c1b9 100644 --- a/apps/eclair/src/shell/components/Header/Header.spec.tsx +++ b/apps/eclair/src/shell/components/Header/Header.spec.tsx @@ -6,7 +6,7 @@ import { describe, expect, it, vi } from 'vitest' import { Header } from './Header' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import { nodeIdSchema, domainNameSchema, moduleNameSchema, graphNameSchema, type GraphName } from '@/platform/domain/eclair-types' diff --git a/apps/eclair/src/shell/components/Header/Header.tsx b/apps/eclair/src/shell/components/Header/Header.tsx index a1e8ace2d..dae35ad78 100644 --- a/apps/eclair/src/shell/components/Header/Header.tsx +++ b/apps/eclair/src/shell/components/Header/Header.tsx @@ -2,7 +2,7 @@ import { useState, useRef, useEffect } from 'react' import { useNavigate } from 'react-router-dom' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import type { GraphName } from '@/platform/domain/eclair-types' import { SchemaModal } from '@/shell/components/SchemaModal/SchemaModal' import { useGraph } from '@/platform/infra/graph-state/GraphContext' @@ -38,7 +38,7 @@ export function Header({ const orphanIds = query.detectOrphans() return { hasOrphans: orphanIds.length > 0, - orphanNodeIds: new Set(orphanIds), + orphanNodeIds: new Set(orphanIds.map((componentId) => componentId.value)), orphanCount: orphanIds.length, } })() diff --git a/apps/eclair/src/shell/components/SchemaModal/SchemaModal.spec.tsx b/apps/eclair/src/shell/components/SchemaModal/SchemaModal.spec.tsx index 03fc6740e..93b171589 100644 --- a/apps/eclair/src/shell/components/SchemaModal/SchemaModal.spec.tsx +++ b/apps/eclair/src/shell/components/SchemaModal/SchemaModal.spec.tsx @@ -8,7 +8,7 @@ import userEvent from '@testing-library/user-event' import { SchemaModal, validateDownloadGraphName } from './SchemaModal' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import { nodeIdSchema, domainNameSchema, moduleNameSchema, graphNameSchema, type GraphName } from '@/platform/domain/eclair-types' diff --git a/apps/eclair/src/shell/components/SchemaModal/SchemaModal.tsx b/apps/eclair/src/shell/components/SchemaModal/SchemaModal.tsx index 0f11e57e8..1b6190450 100644 --- a/apps/eclair/src/shell/components/SchemaModal/SchemaModal.tsx +++ b/apps/eclair/src/shell/components/SchemaModal/SchemaModal.tsx @@ -5,7 +5,7 @@ import { JsonView, collapseAllNested } from 'react-json-view-lite' import 'react-json-view-lite/dist/index.css' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import type { GraphName } from '@/platform/domain/eclair-types' import styles from './SchemaModal.module.css' import { diff --git a/apps/eclair/tsconfig.app.json b/apps/eclair/tsconfig.app.json index 4fc7be698..deafa7efb 100644 --- a/apps/eclair/tsconfig.app.json +++ b/apps/eclair/tsconfig.app.json @@ -36,10 +36,10 @@ "include": ["src"], "references": [ { - "path": "../../packages/riviere-schema/tsconfig.lib.json" + "path": "../../packages/riviere-schema/published-language/tsconfig.lib.json" }, { - "path": "../../packages/riviere-query/tsconfig.lib.json" + "path": "../../packages/riviere-builder/domain-model/tsconfig.lib.json" } ] } diff --git a/apps/eclair/tsconfig.spec.json b/apps/eclair/tsconfig.spec.json index 3d44db5f8..1e1cc7b9f 100644 --- a/apps/eclair/tsconfig.spec.json +++ b/apps/eclair/tsconfig.spec.json @@ -39,10 +39,10 @@ "path": "./tsconfig.app.json" }, { - "path": "../../packages/riviere-schema/tsconfig.lib.json" + "path": "../../packages/riviere-schema/published-language/tsconfig.lib.json" }, { - "path": "../../packages/riviere-query/tsconfig.lib.json" + "path": "../../packages/riviere-builder/tsconfig.lib.json" } ] } diff --git a/docs/architecture/adr/ADR-002-allowed-folder-structures.md b/docs/architecture/adr/ADR-002-allowed-folder-structures.md index 1d376d19c..a2d203646 100644 --- a/docs/architecture/adr/ADR-002-allowed-folder-structures.md +++ b/docs/architecture/adr/ADR-002-allowed-folder-structures.md @@ -4,86 +4,187 @@ ## Sources of Truth -- **Code placement and layer rules:** [`development-skills:separation-of-concerns`](https://github.com/NTCoding/claude-skillz/blob/main/separation-of-concerns/SKILL.md) skill -- **Dependency enforcement:** `.riviere/role-enforcement.config.ts` for first-class layer and role rules; `.dependency-cruiser.mjs` contains legacy rules pending migration to RLE +- **Architecture decision:** this ADR +- **Executable enforcement:** [Rivière role enforcement](../../../.riviere/role-definitions/index.md), configured by [`.riviere/role-enforcement.config.ts`](../../../.riviere/role-enforcement.config.ts) -## Standard Structure +The ADR and executable configuration implement the same rules and must change together. + +## Package Placement + +Every package declared by `pnpm-workspace.yaml` must have exactly one role-enforcement configuration or be explicitly listed as unassigned. + +```text +apps/ +└── {app}/ + +packages/ +└── {subdomain}/ + ├── domain-model/ + ├── published-language/ + └── use-cases/ + +tools/ +└── {tool}/ +``` + +Only a complete path segment named `{subdomain}` creates a subdomain boundary. Other placeholders, including `{tool}` and `{boundary}`, are ordinary path placeholders and never receive subdomain semantics. A package rule may allow imports within the same captured subdomain or across subdomains. Location rules cannot override package rules. + +The executable package assignments are exactly: + +```typescript +'apps/': app, +'packages/{subdomain}/domain-model': domainModel, +'packages/{subdomain}/published-language': publishedLanguage, +'packages/{subdomain}/use-cases': useCases, +'tools/': app, +``` + +Keys ending in `/` assign a configuration to each direct package beneath that directory. Therefore apps and tools are direct packages, while the three subdomain package types must live beneath `packages/{subdomain}/`. + +The repository currently allows these package dependencies: + +- An app may import use-case packages from any subdomain. +- A domain-model package may import published-language packages. +- A published-language package may not import another workspace package. +- A use-case package may import its own subdomain's domain model and published-language packages. +- No package may import an app. + +A tool is an app. Its domain-model, published-language, and use-case packages live under `packages/{subdomain}/`; they cannot be nested under `tools/{tool}/`. + +## App Packages ```text -features/ -├── {feature}/ -│ ├── entrypoint/ ← one folder per external entrypoint -│ │ └── {entrypoint}/ -│ │ ├── entrypoint.ts -│ │ └── ... ← entrypoint-specific DTOs, input mappers, output mappers -│ ├── commands/ ← write operations, strict layering -│ ├── queries/ ← read operations, minimal layering -│ ├── domain/ ← business rules (required if commands exist) -│ │ └── ports/ ← domain-owned capability contracts -│ ├── data-access/ ← aggregate repositories and query-model loaders -│ └── adapters/ ← implementations of domain ports -│ └── {adapter}/ -│ -entrypoint/ -└── _platform/ ← private entrypoint code shared across features +src/ +├── features/ +│ └── {feature}/ +│ └── entrypoint/ +│ ├── {entrypoint}/ +│ └── _platform/ +│ └── cli/ +├── infra/ │ └── cli/ -│ -platform/ -├── domain/ ← shared business rules (depends on nothing) -└── infra/ ← shared technical concerns - ├── external-clients/ ← cohesive third-party tool/service clients - ├── persistence/ ← database clients, connection pools - ├── http/ ← shared formatters, error handling middleware - ├── cli/ ← stdin/stdout utilities, CLI I/O helpers - ├── messaging/ ← queue clients, event bus - ├── config/ ← configuration loading - └── logging/ ← structured logging - -shell/ ← thin wiring/routing only (no business logic) +│ └── presentation/ +└── shell/ +``` + +Features are isolated. Code in one feature cannot import another feature. A feature may import root `infra` and commands or queries exposed by subdomain use-case packages. + +An entrypoint translates an external protocol into a command or query input and translates the result back into that protocol. It performs primitive shape validation only. Domain validation belongs in the command or query. + +For example, an entrypoint passes a raw string through the command input: + +```typescript +export interface LinkExternalInput { + type: string | undefined +} + +const result = linkExternal.execute({ type: options.linkType }) +``` + +The command parses the value through the domain-owned value object. The value object remains the single source of truth for the allowed values: + +```typescript +const parsedType = input.type === undefined ? undefined : LinkType.parse(input.type) +if (parsedType !== undefined && !parsedType.success) { + return failure('VALIDATION_ERROR', parsedType.error) +} +``` + +Do not duplicate the domain's allowed values in an entrypoint union or validator. + +`entrypoint/_platform` contains entrypoint code shared only within that feature's entrypoint location. The `_platform` location is importable anywhere within its parent location and nowhere outside it. + +Root `infra` contains generic technical code. It cannot import application or domain code. CLI presentation formats or writes generic responses. CLI input parsing remains in the entrypoint layer, including shared parsers under a feature's private `entrypoint/_platform/cli` location. + +`shell` wires the application. It may construct external clients and adapters, then pass them into app entrypoints or subdomain use cases. It contains no business decisions. + +## Domain-Model Packages + +```text +src/ +└── domain/ + └── ... ``` -All sub-folders within a feature are optional — include only what the feature needs. +A domain-model package contains one isolated subdomain model. Its internal domain folders are unrestricted. It has no features, entrypoints, use cases, data access, adapters, infra, or shell. + +Domain code owns business state, rules, invariants, value objects, aggregates, domain services, domain events, and domain ports. A domain model does not import another domain model. + +Ports and adapters are preferred for technical capabilities. External-package imports are not globally blocked because valid domain code includes Zod value objects and domains built around libraries such as `ts-morph`. Node capabilities such as `node:path` and `node:perf_hooks` should normally be represented by domain ports. -### Layer Responsibilities +## Use-Case Packages -**entrypoint/** — Contains one folder per external entrypoint: `entrypoint/{entrypoint}/entrypoint.ts`. Opening `entrypoint/` should show the available entrypoints as folders. Entrypoint-specific DTOs, input mappers, and output mappers live under the relevant entrypoint folder. This layer translates between external and internal formats: it parses HTTP requests, CLI arguments, or queue messages into command/query inputs and maps results back to external responses. If you changed protocols (HTTP → CLI), you'd rewrite this layer but keep commands/ and domain/ unchanged. Entrypoints must not import `domain/` or persistence infrastructure directly. +```text +src/ +├── features/ +│ └── {feature}/ +│ ├── commands/ +│ ├── queries/ +│ ├── data-access/ +│ │ └── {concept}/ +│ └── adapters/ +│ └── {adapter}/ +└── infra/ + └── external-clients/ + └── {client}/ +``` + +Features are isolated. A feature cannot import another feature. -Package-level `entrypoint/_platform/` contains private entrypoint code shared across features. Feature-level `entrypoint/_platform/` contains private entrypoint code shared by entrypoints within one feature. Sharing changes scope, not layer. +Commands orchestrate write operations. They accept raw command input, parse domain value objects, load aggregates, invoke domain behaviour, and persist the result. Command input factories belong at the app entrypoint, not in commands. -The `_platform/` convention applies inside any layer. It means code shared within the containing architectural scope, not a globally shared layer. Code outside that containing scope must not import it. +Queries perform read use cases. A query may call multiple methods on the same query model, compose results from multiple query-model methods, map known loader failures into query-use-case errors, or coordinate multiple loaders when the concrete read genuinely needs them. -**commands/** — Orchestrates write operations. Loads data, invokes domain logic, persists the result. All business rules delegated to domain/. Each command has a dedicated input type — no sharing of input DTOs, no dependency on external input types. +A query model is designed for a concrete query use case. For example, `list invalid components` may load an `InvalidComponents` query model, and `show graph statistics` may load `GraphStatistics`. Do not invent a generic `GraphQueryModel` merely because both read the same graph file. -**queries/** — Reads and returns data without modifying anything. Can query the database directly or load domain objects for their state. No side effects, no state changes. +Data access lives at `data-access/{concept}`. Aggregate repositories reconstruct and persist aggregates. Query-model loaders load the concrete query model needed by a query. Data-access failures are `data-access-error`; they are not domain errors. -**domain/** — Business rules with no I/O. Validation, state transitions, invariants, calculations. Never imports from infra/, commands/, queries/, entrypoint/, or shell/. +Data access may import only aggregate and value-object roles from its own domain model. Domain services must not be called from data access. Parsing that protects an aggregate invariant belongs on the aggregate or value object and returns a clear result for the repository to handle. -**domain/ports/** — Domain-owned interfaces and function types for capabilities invoked by the domain. Contracts use domain language and contain no concrete technology types or implementation. +Adapters implement domain ports. An adapter may import only domain-port roles from its own domain model and generic clients from root infra. It translates between the port and client APIs; it does not own business decisions or instantiate shared clients. -**data-access/** — Aggregate repositories and query-model loaders. This layer inherently knows the application state it reconstructs or persists. It is separate from generic infrastructure and must not become a home for domain behaviour. +External clients contain generic interaction with a tool, service, filesystem, runtime, or third-party package. They do not import domain code. -**adapters/** — Narrow implementations of domain ports. A domain-port adapter translates between one domain port and one generic client API. It contains no domain decisions, application orchestration, direct Node API calls, third-party package calls, or coordination across multiple clients. Node and third-party calls belong to the separately enforced generic external-client role; otherwise the adapter would bypass that client contract and combine translation with external I/O. See the [`domain-port-adapter` role definition](../../../.riviere/role-definitions/domain-port-adapter.md) for the concrete Oxlint and GitHub examples. +## Published-Language Packages -**platform/domain/** — Shared business rules used across features. Depends on nothing. +```text +src/ +└── published-language/ + ├── ... + └── eslint-plugin/ ← optional; role enforcement disabled inside this integration +``` -**platform/infra/** — Shared generic technical concerns used across features. It may depend only on other `platform/infra/` code and external libraries. It must not import entrypoint, use-case, domain, or unclassified internal application code. +A published language is a minimal, stable contract intended for consumers across a boundary. It may contain published-language schemas, data structures, unions, parsers, field names, annotations, and value objects. -Each external client stays cohesive under `platform/infra/external-clients/{client}/`. It exposes capabilities and types belonging to the external system, knows nothing about application domain types, and can be extracted into a separate library without taking application code with it. +A published-language parser parses the published language and returns either its declared successful schema shape or its declared failure shape. Application behaviour does not belong in the published language. -For CLI code, platform CLI infrastructure owns shared response-envelope formatting and output side effects. Generic `formatSuccess`/`formatError` style functions are CLI response formatters. Writing to stdout, stderr, files, or exiting belongs to CLI response writers. CLI error handlers are only for uncaught CLI-boundary exceptions and must not handle regular command/query failure control flow. +## Location Rules -**shell/** — Wires things together at startup. It constructs generic clients and domain-port adapters, then passes them into entrypoints or use cases. No business logic and no separate `composition-root` role. +- Locations and imports are unrestricted until a location declares rules. +- Explicit sublocations are the only direct folders permitted inside a location. +- `allowAnySubLocations: true` permits arbitrary domain organisation and cannot be combined with explicit sublocations. +- A location with `importRules` may import its own subtree and the locations listed in `allow`. Every other location is forbidden. +- A sublocation inherits its parent's import rules unless it explicitly disables inheritance. +- Allowing a location allows its entire subtree. +- `sibling` means a configured location under the same concrete parent location. +- `root` means a configured root location in the same package. +- `ownSubdomain` means a location in another package with the same value captured from a complete `{subdomain}` path segment. +- `anySubdomain` means the named location in any package with a value captured from a complete `{subdomain}` path segment. +- A string allows every role in the named location. An object with a role list allows only those roles. +- `_platform` with `importableFrom: 'withinParentLocation'` is private to its parent location. +- Circular imports are rejected. +- Production code cannot import files excluded by `ignorePatterns`. Tests are exempt from production import rules so they can assemble fixtures across boundaries. -## Library Packages +Package configuration keys ending in `/` apply the configuration to each direct package beneath that directory. For example, `'apps/': app` assigns the app configuration to every direct package under `apps/`; it does not assign nested packages. -Libraries use the same `features/` + `platform/` structure as applications. The package is NOT the feature — still wrap in `features/{name}/`. Libraries don't need `shell/` unless they wire an app. +## Package Entry Points -**Entry point:** Libraries use `src/index.ts` as their package entry point — a pure barrel file containing only re-export statements, no logic. `shell/` is for app wiring only, not package exports. +Published packages use `src/index.ts` only as their package entry point. It contains explicit exports from the files that own declarations. Nested barrel files are not allowed. ## Local Exceptions -**React applications** extend the standard feature sub-folders with `components/` and `hooks/`. Entrypoints are page components. Shell contains routing and providers. +`apps/docs` is exempt from role enforcement. -**Flat packages** too small for internal layering (schemas, config, decorators) use flat `src/` with no features/platform/shell structure. +Éclair is explicitly unassigned until it has an approved configuration that honestly describes and enforces its architecture. Its existing Dependency Cruiser rules remain active. -**Claude Code plugin packages** may keep host-required prompt artifacts outside `src/` when the host loader requires fixed top-level locations. For `tools/dev-workflow-v2`, this includes command and state markdown under `tools/dev-workflow-v2/commands/` and `tools/dev-workflow-v2/states/`, hook scripts under `tools/dev-workflow-v2/hooks/`, and plugin metadata under `tools/dev-workflow-v2/.claude-plugin/`. Runtime TypeScript still belongs under `src/`. +Host-required plugin artefacts may live outside `src` when the host mandates their locations. This does not exempt runtime TypeScript from package assignment and role enforcement. diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 23d22beea..e449f88b1 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -19,9 +19,9 @@ Three packages enable deterministic TypeScript extraction: | Package | Purpose | |---------|---------| -| `@living-architecture/riviere-extract-config` | JSON Schema defining the extraction config DSL | -| `@living-architecture/riviere-extract-conventions` | Decorators, default config, and ESLint enforcement | -| `@living-architecture/riviere-extract-ts` | TypeScript extractor using ts-morph for AST parsing | +| `@living-architecture/riviere-extract-config-published-language` | JSON Schema defining the extraction config DSL | +| `@living-architecture/riviere-extract-conventions-published-language` | Decorators, default config, and ESLint enforcement | +| `@living-architecture/riviere-extract-ts-domain-model` | TypeScript extractor using ts-morph for AST parsing | #### Package Dependencies diff --git a/docs/continuous-improvement/post-merge-reflections/2026-02-05-issue-239-m1-d1-8-d1-9-wire-connection-e.md b/docs/continuous-improvement/post-merge-reflections/2026-02-05-issue-239-m1-d1-8-d1-9-wire-connection-e.md index 0893b4a91..cb5e5464e 100644 --- a/docs/continuous-improvement/post-merge-reflections/2026-02-05-issue-239-m1-d1-8-d1-9-wire-connection-e.md +++ b/docs/continuous-improvement/post-merge-reflections/2026-02-05-issue-239-m1-d1-8-d1-9-wire-connection-e.md @@ -166,8 +166,8 @@ Wired `detectConnections` from `riviere-extract-ts` into the `riviere extract` C ### Proposal: Pre-flight layer placement check for simple function calls - **Problem:** 4 architecture review iterations (~40 minutes) to place `detectConnections` correctly. Moved commands/ → queries/ → split query/presentation → inlined in entrypoint. -- **Root cause:** No guidance on when a function call is too simple for commands/ or queries/ wrappers. The decision tree in separation-of-concerns covers _what_ belongs where, but doesn't address the case where a single function call has no orchestration value. -- **Proposed change:** Add guidance to separation-of-concerns or the task workflow: "Before creating a command or query wrapper, check if the function being called requires orchestration (loading, domain logic, persistence). If it's a single function call with no orchestration, inline it in the entrypoint." +- **Root cause:** No local guidance on when a function call is too simple for commands/ or queries/ wrappers. The role selection guide covers _what_ belongs where, but doesn't address the case where a single function call has no orchestration value. +- **Proposed change:** Add guidance to the local role selection guide or the task workflow: "Before creating a command or query wrapper, check if the function being called requires orchestration (loading, domain logic, persistence). If it's a single function call with no orchestration, inline it in the entrypoint." - **Expected impact:** ~30 minutes saved per occurrence (eliminates multiple architecture review iterations for trivial wrappers). ### Proposal: Run architecture review before first commit, not after @@ -183,4 +183,4 @@ Wired `detectConnections` from `riviere-extract-ts` into the `riviere extract` C - ✅ Added RFC-016: Mock Cleanup After vi.spyOn (review-feedback-checks.md) - Add RFC: "Prefer for-of over forEach for side-effect-only iteration" - Create task: Add pre-implementation placement check to task workflow -- Create task: Add "single function call = inline in entrypoint" guidance to separation-of-concerns +- Create task: Add "single function call = inline in entrypoint" guidance to the local role selection guide diff --git a/docs/conventions/review-feedback-checks.md b/docs/conventions/review-feedback-checks.md index 90a205949..939d015c4 100644 --- a/docs/conventions/review-feedback-checks.md +++ b/docs/conventions/review-feedback-checks.md @@ -125,7 +125,7 @@ interface DraftComponent { location: { file: string; line: number } } ``` -The type is identical to what's exported from `@living-architecture/riviere-extract-ts`. This creates: +The type is identical to what's exported from `@living-architecture/riviere-extract-ts-domain-model`. This creates: - Maintenance burden: changes in source require manual sync - Potential drift: types can diverge silently - Wasted code: duplication with no benefit @@ -149,7 +149,7 @@ interface DraftComponent { **Example (GOOD):** ```typescript -import { type DraftComponent } from '@living-architecture/riviere-extract-ts' +import { type DraftComponent } from '@living-architecture/riviere-extract-ts-domain-model' ``` **Detection:** Local interface/type definitions that match exported types from project packages. Check if the local definition could be replaced with an import. diff --git a/docs/design-reviews/eclair/critique.md b/docs/design-reviews/eclair/critique.md index 60632c1b8..1caaa493d 100644 --- a/docs/design-reviews/eclair/critique.md +++ b/docs/design-reviews/eclair/critique.md @@ -33,7 +33,7 @@ Reviewed: docs/design-reviews/eclair/refined.md ### pluralize.ts in Domain Map is Generic Infrastructure - **What's wrong:** The refined design places `pluralize.ts` in `features/domain-map/`. Pluralization is a generic text utility, not domain-specific to domain maps. -- **Why it matters:** Violates separation-of-concerns principle 2 (separate generic from domain-specific). When other features need pluralization, they will either duplicate or import across feature boundaries. +- **Why it matters:** Violates the local rule separating generic code from domain-aware code. When other features need pluralization, they will either duplicate or import across feature boundaries. - **Suggested fix:** Move to `platform/infra/text/` or similar generic location. ### LayoutPosition Value Object Not Actually Needed @@ -54,7 +54,7 @@ Reviewed: docs/design-reviews/eclair/refined.md ## MEDIUM ### errors.ts at Root Violates Structure -- **What's wrong:** The design shows `errors.ts` at package root. But per separation-of-concerns, there should be no generic type-grouping files spanning multiple capabilities. +- **What's wrong:** The design shows `errors.ts` at package root. The local architecture rules do not permit generic type-grouping files spanning multiple capabilities. - **Why it matters:** A single errors.ts grows to contain GraphError, RenderingError, LayoutError, ContextError, CSSModuleError, DOMError, SchemaError. These belong to different layers and change for different reasons. - **Suggested fix:** Split errors to live with their associated capabilities: GraphError with graph handling, LayoutError with layout, etc. @@ -97,8 +97,8 @@ Reviewed: docs/design-reviews/eclair/refined.md ### ArchitectureMetrics Interface is Anemic - **What's wrong:** The proposed `ArchitectureMetrics` is a pure data structure with readonly number fields. It has no behavior. -- **Why it matters:** This is fine for a DTO/read model, but the design calls it a "value object" which implies it should have behavior. It's just a plain interface. -- **Suggested fix:** Either add meaningful methods (comparisons, validation) or correctly label it as a read model, not a value object. +- **Why it matters:** This is fine for a DTO/query model, but the design calls it a "value object" which implies it should have behavior. It's just a plain interface. +- **Suggested fix:** Either add meaningful methods (comparisons, validation) or correctly label it as a query model, not a value object. ### GraphContext.tsx Proposed Location is Confusing - **What's wrong:** The design places `GraphContext.tsx` in `platform/domain/riviere/`. But contexts are React infrastructure, not domain logic. diff --git a/docs/design-reviews/eclair/refined.md b/docs/design-reviews/eclair/refined.md index 0b9a4d41a..3fbc37f4d 100644 --- a/docs/design-reviews/eclair/refined.md +++ b/docs/design-reviews/eclair/refined.md @@ -346,7 +346,7 @@ Each feature projects a specialized view from the loaded RiviereGraph: ## Notes on React Adaptation -The separation-of-concerns pattern adapts to React as follows: +The local architecture rules adapt to React as follows: - **entrypoint/** becomes Page components that receive props and render - **use-cases/** becomes the orchestration within Page components (useMemo + projection function + state hooks) diff --git a/docs/design-reviews/eclair/refinements.md b/docs/design-reviews/eclair/refinements.md index 77781a0ed..2d1af4f84 100644 --- a/docs/design-reviews/eclair/refinements.md +++ b/docs/design-reviews/eclair/refinements.md @@ -1,6 +1,6 @@ # Refinements for eclair -Refinements based on separation-of-concerns and tactical-ddd skill principles. +Refinements based on the repository's local architecture rules and role definitions. ## Separation of Concerns Refinements diff --git a/docs/design-reviews/riviere-builder/critique.md b/docs/design-reviews/riviere-builder/critique.md deleted file mode 100644 index 283444c24..000000000 --- a/docs/design-reviews/riviere-builder/critique.md +++ /dev/null @@ -1,133 +0,0 @@ -# Critique for riviere-builder - -Reviewed: docs/design-reviews/riviere-builder/refined.md - -## CRITICAL - -### save() method still exists with Node.js filesystem imports - -- **What's wrong:** The refined design explicitly states "Remove `save()` method from `RiviereBuilder`" to achieve browser compatibility and domain isolation. However, the actual code at `packages/riviere-builder/src/builder.ts` lines 1-2 imports `node:fs` and `node:path`, and lines 864-876 implement the `save()` method with filesystem I/O. -- **Why it matters:** This is a direct contradiction between the refined design and implementation. The package cannot be browser-compatible while bundling Node.js filesystem APIs. Infrastructure concerns (filesystem I/O) are polluting the domain, violating DDD principle 1 (isolate domain logic from infrastructure). -- **Suggested fix:** Remove `save()` method and the Node.js imports. Update documentation to show callers using `builder.build()` followed by their own filesystem write. - -### DirectoryNotFoundError exists only to support save() - -- **What's wrong:** The `DirectoryNotFoundError` class in `errors.ts` (lines 129-137) exists solely to support the `save()` method's directory existence check. If `save()` is removed per the design, this error class becomes orphaned infrastructure-specific code. -- **Why it matters:** The error hierarchy contains infrastructure-specific errors mixed with domain errors, violating separation of concerns. Domain errors should describe business rule violations, not filesystem problems. -- **Suggested fix:** Remove `DirectoryNotFoundError` when removing `save()`. - -## HIGH - -### RiviereBuilder is a God Class with too many responsibilities - -- **What's wrong:** The `RiviereBuilder` class (builder.ts) spans 877 lines and handles: graph construction, component registration, linking, enrichment, inspection (stats, warnings, orphans), validation, serialization, querying, and file I/O. This violates both separation of concerns (multiple unrelated responsibilities) and DDD principle 3 (use cases should be distinct intentions). -- **Why it matters:** The refined design proposes splitting into features (graph-construction, graph-enrichment, graph-inspection, error-recovery) but the current implementation bundles everything. Changes to validation logic could accidentally affect linking. Testing requires the entire class. -- **Suggested fix:** Extract inspection methods (`warnings()`, `stats()`, `orphans()`, `validate()`) to a separate `GraphInspector` class. Extract `query()` since it creates a different object. Keep `RiviereBuilder` focused on construction and linking. - -### Internal graph state is publicly exposed - -- **What's wrong:** Line 133: `graph: BuilderGraph` is a public field. External code can directly mutate `builder.graph.components.push(...)` or `builder.graph.links = []`, bypassing all invariant enforcement. -- **Why it matters:** This violates DDD principle 7 (aggregates protect invariants). The duplicate component check in `registerComponent()`, domain existence validation, and all other safeguards can be circumvented. The aggregate boundary is meaningless if external code can access internal state directly. -- **Suggested fix:** Make `graph` private: `private graph: BuilderGraph`. Add explicit getter methods if read-only access is needed. - -### types.ts is a generic type-grouping file spanning multiple capabilities - -- **What's wrong:** The `types.ts` file contains 185 lines of types spanning: builder options, all component inputs (UI, API, UseCase, DomainOp, Event, EventHandler, Custom), link inputs, near-match types, stats types, warning types, and enrichment types. These serve completely different features. -- **Why it matters:** This violates SoC checklist item 13 ("no generic type-grouping files spanning multiple capabilities"). When adding a new component type, you modify the same file as when changing warning behavior. Types are not co-located with their usage. -- **Suggested fix:** Split per the refined design: inspection-types.ts (BuilderStats, BuilderWarning, WarningCode), match-types.ts (NearMatchQuery, NearMatchResult, NearMatchMismatch, NearMatchOptions), and component inputs with their respective add methods or in a dedicated construction-types.ts. - -### errors.ts is a generic error-grouping file - -- **What's wrong:** All 12 error classes are in a single `errors.ts` file spanning: domain errors (DuplicateDomainError, DomainNotFoundError), component errors (DuplicateComponentError, ComponentNotFoundError), custom type errors (CustomTypeNotFoundError, CustomTypeAlreadyDefinedError, MissingRequiredPropertiesError), enrichment errors (InvalidEnrichmentTargetError), graph errors (InvalidGraphError), and validation errors (BuildValidationError, MissingSourcesError, MissingDomainsError), plus infrastructure error (DirectoryNotFoundError). -- **Why it matters:** Errors for unrelated features are coupled together. The refined design proposes feature-specific error files: construction-errors.ts, enrichment-errors.ts, validation-errors.ts, lookup-errors.ts. -- **Suggested fix:** Co-locate errors with the code that throws them, per the refined design. - -## MEDIUM - -### builder-internals.ts mixes concerns - -- **What's wrong:** `builder-internals.ts` contains four unrelated functions: `generateComponentId` (ID generation), `createComponentNotFoundError` (error creation with suggestions), `validateDomainExists`, `validateCustomType`, `validateRequiredProperties` (all validation wrappers). These functions have different reasons to change and different callers. -- **Why it matters:** The file name "internals" is vague and doesn't describe what the functions have in common. SoC principle 5: "Separate functions that don't have related names." -- **Suggested fix:** Move `generateComponentId` to `domain/component-id-generator.ts`. Keep assertions in `builder-assertions.ts`. Move error creation to `error-recovery/` or co-locate with `ComponentNotFoundError`. - -### deduplicate.ts mixes generic and domain-specific deduplication - -- **What's wrong:** `deduplicate.ts` contains two functions: `deduplicateStrings` (generic string array deduplication) and `deduplicateStateTransitions` (domain-specific StateTransition deduplication using from/to/trigger equality). -- **Why it matters:** Generic capabilities should be in `platform/`, domain-specific logic in `features/`. The refined design correctly separates: `deduplicateStrings` to `platform/domain/collection-utils/` and `deduplicateStateTransitions` to `features/graph-enrichment/domain/`. -- **Suggested fix:** Apply the proposed split. The domain-specific function knows about StateTransition equality rules, so it belongs with enrichment domain logic. - -### Validation delegated entirely to external package - -- **What's wrong:** `validateGraph()` in inspection.ts (line 176-178) simply creates a `RiviereQuery` and calls its `validate()` method. All validation logic is outsourced to riviere-query package. -- **Why it matters:** If riviere-builder has its own invariants beyond schema compliance (e.g., checking for orphans as warnings vs errors, verifying enrichment consistency), there's no place to add them. The builder doesn't distinguish its validation concerns from schema validation. -- **Suggested fix:** Consider whether builder-specific validation rules exist. If so, compose them with schema validation. Document explicitly that validation is purely schema-based if that's intentional. - -### enrichComponent mutates argument directly - -- **What's wrong:** The `enrichComponent` method (lines 589-616) directly mutates the component object it finds: `component.entity = enrichment.entity`, `component.stateChanges = [...]`. This is mutation-in-place rather than immutable state transitions. -- **Why it matters:** While the builder is mutable by design, direct mutation makes it harder to reason about state changes and track what changed. It also means partial failures could leave the component in an inconsistent state (e.g., if `behavior` merge throws after `entity` was already set). -- **Suggested fix:** Consider creating a new component object with all changes applied atomically, then replacing it in the components array. This makes the operation transactional. - -### resume() performs minimal validation - -- **What's wrong:** `RiviereBuilder.resume()` (lines 157-174) only checks that sources exist. It doesn't verify: domain existence, component ID validity, link target existence, custom type consistency, or schema compliance. -- **Why it matters:** A malformed graph can be loaded and corrupt the builder state. Subsequent operations might fail in confusing ways. The invariants that `RiviereBuilder.new()` enforces are not enforced on resume. -- **Suggested fix:** Call `validateGraph()` on the input before restoring. Document that resume accepts only valid RiviereGraph objects. Consider whether partial/draft graphs should use a different restoration mechanism. - -### NearMatchQuery uses primitive string for name instead of value object - -- **What's wrong:** `NearMatchQuery` uses `name: string` for the search term. The refined design mentions using `ComponentId` value object from schema, but the near-match logic operates on raw strings. -- **Why it matters:** Inconsistent use of value objects. The `createSourceNotFoundError` does extract `id.name()` from a `ComponentId`, but the general `findNearMatches` API accepts raw strings, losing type safety. -- **Suggested fix:** Either accept `ComponentId` for the query and extract parts internally, or document that the string-based API is intentional for flexibility. - -## LOW - -### Inconsistent function naming: assert vs validate - -- **What's wrong:** `builder-assertions.ts` has functions named `assertDomainExists`, `assertCustomTypeExists`, `assertRequiredPropertiesProvided`. `builder-internals.ts` wraps these as `validateDomainExists`, `validateCustomType`, `validateRequiredProperties`. Two naming conventions for the same concept. -- **Why it matters:** Cognitive overhead. Are "assert" and "validate" semantically different? Both throw errors. The indirection adds no value. -- **Suggested fix:** Pick one naming convention. If "assert" is preferred (implies throwing), remove the validate wrappers and call assert functions directly. - -### Feature directory structure not implemented - -- **What's wrong:** The refined design proposes: `features/graph-construction/`, `features/graph-enrichment/`, `features/graph-inspection/`, `features/error-recovery/`, and `platform/domain/`. The actual code is flat: `src/builder.ts`, `src/types.ts`, `src/errors.ts`, etc. -- **Why it matters:** The refined design analysis passes its own checklist, but the implementation doesn't match. The design document is aspirational, not descriptive. -- **Suggested fix:** Either implement the proposed structure or update the design document to reflect reality and explain why the flat structure is acceptable for a ~600 line package. - -### Missing domain/ folder in features per SoC pattern - -- **What's wrong:** If implementing the refined structure, features like `graph-inspection` have multiple domain files but no orchestrating use-case. The refined design acknowledges "no separate use-cases needed - they are called directly by the builder" but this means the builder IS the use-case for all features. -- **Why it matters:** The SoC pattern expects features to have their own entry points. Having one mega-class orchestrate four features' worth of domain logic doesn't achieve the separation the pattern intends. -- **Suggested fix:** Consider whether the builder should be split into multiple collaborating classes (GraphConstructor, GraphEnricher, GraphInspector, etc.) that are composed together, rather than one class calling into domain functions. - -### ComponentId.parse used inconsistently - -- **What's wrong:** In `createComponentNotFoundError` (builder-internals.ts line 25), the code does `ComponentId.parse(id)` on a string that was already generated by `generateComponentId`. The ID format is internal knowledge duplicated across parsing and generation. -- **Why it matters:** If the ID format changes, both places need updating. The generation function returns a string but the error recovery needs the parsed value object. -- **Suggested fix:** Have `generateComponentId` return a `ComponentId` value object. Callers that need the string can call `toString()`. This ensures the format is defined in one place. - -### No explicit state machine for graph construction phases - -- **What's wrong:** The builder allows adding components, linking, enriching, and building in any order. There's no explicit modeling of construction phases (e.g., "defining structure" vs "linking" vs "enriching" vs "finalizing"). -- **Why it matters:** DDD principle 6 suggests making implicit concepts explicit. If there are constraints about what operations are valid when (e.g., can you enrich after build? can you add components after linking?), these aren't enforced or documented. -- **Suggested fix:** Document the intended usage pattern. If phase constraints exist, consider modeling them (e.g., `builder.finalize()` returns an immutable object that prevents further mutation). - -### menu test ambiguity for inspection methods - -- **What's wrong:** The refined design claims `warnings()`, `stats()`, `orphans()` pass the menu test as user intentions. But these are more like queries on graph state rather than actions a user would request from a "menu" of features. -- **Why it matters:** DDD principle 3 menu test: "If you described your application's features to a user like a menu, would this be on it?" A user might say "Build graph" but probably not "Calculate stats." -- **Suggested fix:** Consider reframing: these are not use cases, they're query methods on the aggregate. This is fine, but don't claim they pass the menu test. They're legitimate read operations, just not "intentions." - -## Summary - -The most critical issues are: - -1. **save() method contradicts the refined design** - Must be removed for browser compatibility and domain isolation. This is an explicit design decision that wasn't implemented. - -2. **Public graph field violates aggregate invariant protection** - Making this private is a minimal fix with high impact on correctness guarantees. - -3. **RiviereBuilder is doing too much** - Consider extracting inspection and query capabilities to separate classes. - -4. **Centralized types.ts and errors.ts files** - Split per the refined design to achieve co-location and cohesion. - -The refined design document is well-reasoned but represents aspirational architecture, not current implementation. The gap between design and code needs reconciliation - either implement the proposed structure or update the design to justify the simpler flat structure for this package size. diff --git a/docs/design-reviews/riviere-builder/design.md b/docs/design-reviews/riviere-builder/design.md deleted file mode 100644 index ae477873c..000000000 --- a/docs/design-reviews/riviere-builder/design.md +++ /dev/null @@ -1,281 +0,0 @@ -# Separation of Concerns Analysis: riviere-builder - -## Package Overview - -**Package:** `@living-architecture/riviere-builder` -**Location:** `/Users/nicko/code/living-architecture-issue-203-architecture-review-and-adr-fo/packages/riviere-builder/` -**Purpose:** Construct Riviere architecture graphs programmatically via a fluent builder API - -## Current Structure - -```text -packages/riviere-builder/ -└── src/ - ├── index.ts # Public exports - ├── builder.ts # RiviereBuilder class (main API) - ├── builder-assertions.ts # Domain existence checks - ├── builder-internals.ts # ID generation, validation wrappers - ├── builder-test-fixtures.ts # Test helpers - ├── component-suggestion.ts # Fuzzy matching for error messages - ├── deduplicate.ts # Array deduplication utilities - ├── errors.ts # All error classes - ├── inspection.ts # Graph analysis functions - ├── merge-behavior.ts # Behavior merge logic - ├── string-similarity.ts # Levenshtein distance algorithm - └── types.ts # All input/output type definitions -``` - -## Checklist Evaluation - -### 1. Verify features/, platform/, shell/ exist at root - -**Status:** FAIL - -The package uses a flat `src/` structure. No `features/`, `platform/`, or `shell/` directories exist. - -### 2. Verify platform/ contains only domain/ and infra/ - -**Status:** N/A (no platform/ directory exists) - -### 3. Verify each feature contains only entrypoint/, use-cases/, domain/ - -**Status:** N/A (no features/ directory exists) - -### 4. Verify shell/ contains no business logic - -**Status:** N/A (no shell/ directory exists) - -### 5. Verify code belonging to one feature is in features/[feature]/ - -**Status:** FAIL - -All code is in a flat structure. The package has identifiable features that are not separated: -- **Graph Building:** Component addition, linking, serialization -- **Graph Inspection:** Validation, statistics, orphan detection, warnings -- **Error Recovery:** Near-match suggestions, fuzzy string matching - -### 6. Verify shared business logic is in platform/domain/ - -**Status:** FAIL - -Shared logic (deduplication, string similarity) is scattered in root `src/`. - -### 7. Verify external service wrappers are in platform/infra/ - -**Status:** N/A - -This package has no external service dependencies. It uses only Node.js `fs` for file I/O in the `save()` method of `builder.ts`. - -### 8. Verify custom folders are inside domain/, not use-cases/ - -**Status:** N/A (no use-cases/ directory exists) - -### 9. Verify each function relies on same state as others in its class/file - -**Status:** PARTIAL PASS - -- `builder.ts`: The `RiviereBuilder` class methods all operate on `this.graph` (same state) -- `errors.ts`: All error classes are stateless value objects (cohesive) -- `types.ts`: All type definitions (no functions, cohesive) -- `inspection.ts`: Functions operate on `InspectionGraph` parameter (cohesive) -- `component-suggestion.ts`: Functions operate on component arrays (cohesive) -- `string-similarity.ts`: Pure functions on string parameters (cohesive) - -However: -- `builder-internals.ts`: Mixes ID generation with validation delegation (different concerns) - -### 10. Verify each file name relates to other files in its directory - -**Status:** PARTIAL FAIL - -All files are in root `src/`, making naming relationships unclear. Some names relate: -- `builder.ts`, `builder-internals.ts`, `builder-assertions.ts` (builder family) -- `component-suggestion.ts`, `string-similarity.ts` (suggestion family) - -But others are loosely related: -- `deduplicate.ts` (utility) -- `merge-behavior.ts` (specific to DomainOp enrichment) -- `inspection.ts` (graph analysis) - -### 11. Verify each directory name describes what all files inside have in common - -**Status:** FAIL - -Only one directory (`src/`) contains all code. The name does not describe the commonality. - -### 12. Verify use-cases/ contains only use-case files - -**Status:** N/A (no use-cases/ directory exists) - -### 13. Verify no generic type-grouping files spanning multiple capabilities - -**Status:** FAIL - -- `types.ts`: Contains 15+ interfaces spanning building, linking, enrichment, matching, warnings -- `errors.ts`: Contains 12 error classes spanning domains, components, validation, custom types - -### 14. Verify entrypoint/ is thin and never imports from domain/ - -**Status:** N/A (no entrypoint/ directory exists) - -## Analysis Summary - -### Identified Features (Capabilities) - -1. **graph-construction**: Building graphs with components and links - - `builder.ts` (RiviereBuilder class) - - `builder-internals.ts` (ID generation) - - `builder-assertions.ts` (validation) - -2. **graph-enrichment**: Enriching DomainOp components - - `merge-behavior.ts` - - `deduplicate.ts` (shared utility) - -3. **graph-inspection**: Analyzing graph state - - `inspection.ts` (stats, orphans, warnings, validation) - -4. **error-recovery**: Suggesting alternatives on errors - - `component-suggestion.ts` - - `string-similarity.ts` - -### Platform Candidates (Shared Logic) - -- `string-similarity.ts`: Generic Levenshtein distance algorithm -- `deduplicate.ts`: Generic array deduplication - -### Principle Violations - -#### Principle 2: Separate feature-specific from shared capabilities - -`string-similarity.ts` is a generic algorithm useful beyond this package. It belongs in `platform/domain/` as shared logic. - -`deduplicate.ts` contains generic deduplication logic. The string deduplication is generic; the state transition deduplication is domain-specific to Riviere graphs. - -#### Principle 3: Separate intent from execution - -`builder.ts` mixes high-level flow with some implementation details: -- Component creation methods follow a consistent pattern (good) -- The `enrichComponent` method has implementation details interleaved with intent - -#### Principle 5: Separate functions that don't have related names - -`builder-internals.ts` contains: -- `generateComponentId()` - ID generation -- `createComponentNotFoundError()` - Error creation -- `validateDomainExists()`, `validateCustomType()`, `validateRequiredProperties()` - Validation wrappers - -These functions have unrelated names and purposes, grouped only by being "internal" to the builder. - -## Recommended Structure - -```text -packages/riviere-builder/ -└── src/ - ├── features/ - │ ├── graph-construction/ - │ │ ├── entrypoint/ - │ │ │ └── riviere-builder.ts # Public builder class - │ │ ├── use-cases/ - │ │ │ ├── add-component.ts # Component addition logic - │ │ │ ├── create-link.ts # Linking logic - │ │ │ └── build-graph.ts # Final build/validation - │ │ └── domain/ - │ │ ├── component-id.ts # ID generation - │ │ ├── graph-state.ts # BuilderGraph type - │ │ └── validation.ts # Domain/type assertions - │ │ - │ ├── graph-enrichment/ - │ │ ├── entrypoint/ - │ │ │ └── enrichment-api.ts # Public enrichment methods - │ │ ├── use-cases/ - │ │ │ └── enrich-domain-op.ts # Enrichment orchestration - │ │ └── domain/ - │ │ ├── merge-behavior.ts # Behavior merging rules - │ │ └── state-transition.ts # State transition dedup - │ │ - │ ├── graph-inspection/ - │ │ ├── entrypoint/ - │ │ │ └── inspection-api.ts # Public inspection methods - │ │ └── domain/ - │ │ ├── find-orphans.ts # Orphan detection - │ │ ├── calculate-stats.ts # Statistics - │ │ ├── find-warnings.ts # Warning detection - │ │ └── graph-validation.ts # Schema validation - │ │ - │ └── error-recovery/ - │ ├── entrypoint/ - │ │ └── suggestion-api.ts # Public suggestion methods - │ └── domain/ - │ ├── near-match.ts # Fuzzy component matching - │ └── mismatch-detection.ts # Type/domain mismatch - │ - ├── platform/ - │ └── domain/ - │ ├── string-similarity/ - │ │ └── levenshtein.ts # Generic string similarity - │ └── array-deduplication/ - │ └── deduplicate-strings.ts # Generic deduplication - │ - └── shell/ - └── index.ts # Public API exports -``` - -## Key Findings - -| Finding | Severity | Location | -|---------|----------|----------| -| Flat structure missing features/platform/shell | High | `/packages/riviere-builder/src/` | -| Generic types.ts spans multiple capabilities | Medium | `/packages/riviere-builder/src/types.ts` | -| Generic errors.ts spans multiple capabilities | Medium | `/packages/riviere-builder/src/errors.ts` | -| builder-internals.ts mixes unrelated functions | Medium | `/packages/riviere-builder/src/builder-internals.ts` | -| Generic algorithms not in platform/ | Low | `string-similarity.ts`, `deduplicate.ts` | - -## Recommendations - -1. **Introduce feature directories** to separate graph-construction, graph-enrichment, graph-inspection, and error-recovery capabilities - -2. **Split types.ts** into feature-specific types co-located with their features: - - `graph-construction/domain/input-types.ts` - - `graph-inspection/domain/stats-types.ts` - - `error-recovery/domain/match-types.ts` - -3. **Split errors.ts** into feature-specific errors: - - `graph-construction/domain/construction-errors.ts` - - `graph-enrichment/domain/enrichment-errors.ts` - -4. **Extract platform/domain/** for truly generic utilities: - - `string-similarity/` - Levenshtein algorithm - - `array-deduplication/` - Generic dedup - -5. **Decompose builder-internals.ts** by responsibility: - - ID generation into `graph-construction/domain/` - - Validation wrappers into `graph-construction/domain/` - -6. **Create shell/index.ts** as the single public API surface that composes and exports the features - -## Trade-offs - -**Current structure benefits:** -- Simple navigation (few files) -- Easy to understand for small codebase -- Matches existing project conventions in the `development-skills:separation-of-concerns` skill - -**Recommended structure benefits:** -- Clear separation of concerns -- Easier to test features in isolation -- Easier to evolve features independently -- Types/errors co-located with usage - -**Migration risk:** -- Breaking changes to imports -- Increased file count -- More complex navigation initially - -## Conclusion - -The `riviere-builder` package violates several separation of concerns principles due to its flat structure. The most significant issues are: -1. No feature-based organization -2. Generic types and errors spanning multiple capabilities -3. Mixed responsibilities in `builder-internals.ts` - -For a package of this size (~10 source files), the current structure is maintainable but will not scale well. If the package grows or requires significant modification, restructuring according to the recommended layout would improve maintainability and testability. diff --git a/docs/design-reviews/riviere-builder/refined.md b/docs/design-reviews/riviere-builder/refined.md deleted file mode 100644 index 5b8970e09..000000000 --- a/docs/design-reviews/riviere-builder/refined.md +++ /dev/null @@ -1,330 +0,0 @@ -# Refined Design: riviere-builder - -## Package Overview - -**Package:** `@living-architecture/riviere-builder` -**Location:** `packages/riviere-builder/` -**Purpose:** Construct Riviere architecture graphs programmatically via a fluent builder API - ---- - -## Refined Structure - -```text -packages/riviere-builder/ -└── src/ - ├── index.ts # Public API exports (shell for libraries) - │ - ├── features/ - │ ├── graph-construction/ - │ │ ├── domain/ - │ │ │ ├── component-id-generator.ts # ID generation using ComponentId value object - │ │ │ ├── graph-under-construction.ts # BuilderGraph type and invariants - │ │ │ ├── domain-assertions.ts # Domain/custom-type existence checks - │ │ │ └── construction-errors.ts # DuplicateDomainError, DomainNotFoundError, etc. - │ │ └── use-cases/ - │ │ └── riviere-builder.ts # RiviereBuilder class (fluent API) - │ │ - │ ├── graph-enrichment/ - │ │ └── domain/ - │ │ ├── behavior-merger.ts # mergeBehavior logic - │ │ ├── state-transition-dedup.ts # Domain-specific deduplication - │ │ └── enrichment-errors.ts # InvalidEnrichmentTargetError - │ │ - │ ├── graph-inspection/ - │ │ └── domain/ - │ │ ├── orphan-detector.ts # findOrphans - │ │ ├── stats-calculator.ts # calculateStats - │ │ ├── warning-detector.ts # findWarnings - │ │ ├── graph-validator.ts # validateGraph - │ │ ├── graph-converter.ts # toRiviereGraph - │ │ ├── inspection-types.ts # BuilderStats, BuilderWarning, WarningCode - │ │ └── validation-errors.ts # BuildValidationError, InvalidGraphError - │ │ - │ └── error-recovery/ - │ └── domain/ - │ ├── near-match-finder.ts # findNearMatches - │ ├── mismatch-detector.ts # detectMismatch - │ ├── suggestion-generator.ts # createSourceNotFoundError - │ ├── match-types.ts # NearMatchQuery, NearMatchResult, etc. - │ └── lookup-errors.ts # ComponentNotFoundError - │ - └── platform/ - └── domain/ - ├── text-similarity/ - │ └── levenshtein.ts # levenshteinDistance, similarityScore - └── collection-utils/ - └── deduplicate-strings.ts # deduplicateStrings (generic) -``` - ---- - -## Separation of Concerns Checklist (Refined) - -### 1. Verify features/, platform/, shell/ exist at root - -**Status:** PASS (adapted for library pattern) - -- `features/` contains feature-specific code -- `platform/` contains shared generic utilities -- `index.ts` serves as public API surface (library equivalent of shell) - -### 2. Verify platform/ contains only domain/ and infra/ - -**Status:** PASS - -- `platform/domain/` contains generic algorithms -- No `infra/` needed - this package has no external service dependencies (filesystem I/O removed per DDD-1) - -### 3. Verify each feature contains only entrypoint/, use-cases/, domain/ - -**Status:** PASS (adapted) - -- `graph-construction/` has `use-cases/` (RiviereBuilder) and `domain/` -- Other features have only `domain/` (no separate use-cases needed - they are called directly by the builder) - -### 4. Verify shell/ contains no business logic - -**Status:** PASS - -- `index.ts` contains only re-exports - -### 5. Verify code belonging to one feature is in features/[feature]/ - -**Status:** PASS - -Four features clearly separated: -- `graph-construction/` - building graphs with components and links -- `graph-enrichment/` - enriching DomainOp components -- `graph-inspection/` - analyzing graph state -- `error-recovery/` - suggesting alternatives on errors - -### 6. Verify shared business logic is in platform/domain/ - -**Status:** PASS - -- `text-similarity/` - Levenshtein algorithm (generic) -- `collection-utils/` - string deduplication (generic) - -### 7. Verify external service wrappers are in platform/infra/ - -**Status:** N/A - -- Filesystem I/O removed from builder (callers handle I/O) -- No external service dependencies - -### 8. Verify custom folders are inside domain/, not use-cases/ - -**Status:** PASS - -- No custom folders outside defined structure - -### 9. Verify each function relies on same state as others in its class/file - -**Status:** PASS - -Each file now contains cohesive functions: -- `behavior-merger.ts` - merging behavior objects -- `orphan-detector.ts` - finding orphans -- `near-match-finder.ts` - fuzzy matching - -### 10. Verify each file name relates to other files in its directory - -**Status:** PASS - -Files grouped by feature. Names within each feature relate: -- `graph-inspection/domain/`: orphan-detector, stats-calculator, warning-detector, graph-validator -- `error-recovery/domain/`: near-match-finder, mismatch-detector, suggestion-generator - -### 11. Verify each directory name describes what all files inside have in common - -**Status:** PASS - -- `graph-construction/` - files for constructing graphs -- `graph-enrichment/` - files for enriching components -- `graph-inspection/` - files for inspecting/analyzing graphs -- `error-recovery/` - files for error recovery and suggestions - -### 12. Verify use-cases/ contains only use-case files - -**Status:** PASS - -- `graph-construction/use-cases/` contains only `riviere-builder.ts` - -### 13. Verify no generic type-grouping files spanning multiple capabilities - -**Status:** PASS - -Types co-located with features: -- `inspection-types.ts` in `graph-inspection/domain/` -- `match-types.ts` in `error-recovery/domain/` - -### 14. Verify entrypoint/ is thin and never imports from domain/ - -**Status:** N/A (library pattern) - -- No entrypoint/ directories -- `RiviereBuilder` class is in `use-cases/` and properly orchestrates domain operations - ---- - -## Tactical DDD Checklist (Refined) - -### 1. Verify domain is isolated from infrastructure - -**Status:** PASS (after refinement) - -**Change:** Remove `save()` method from `RiviereBuilder`. Callers use: -```typescript -const graph = builder.build() -await fs.writeFile(path, JSON.stringify(graph, null, 2)) -``` - -This removes Node.js `fs` dependency and makes the package browser-compatible. - -### 2. Verify names are from YOUR domain, not generic developer jargon - -**Status:** PASS - -Domain terms used consistently: -- Component, Domain, Link, Graph (from Riviere domain) -- Enrichment (domain concept for adding details) -- NearMatch (domain concept for error recovery) -- DomainOp, UseCase, Event, EventHandler (component types) - -### 3. Verify use cases are intentions of users (menu test) - -**Status:** PASS - -User intentions (what would appear in a menu): -- Create new graph configuration -- Add component to graph -- Link components together -- Enrich component with details -- Find similar components (for error recovery) -- Inspect graph (stats, warnings, orphans) -- Validate graph -- Build final graph - -All these are represented as methods on `RiviereBuilder`. - -### 4. Verify business logic lives in domain objects, use cases only orchestrate - -**Status:** PASS - -Business logic properly placed: -- `mergeBehavior()` - knows how to merge operation behaviors -- `deduplicateStateTransitions()` - knows equality rules for transitions -- `findNearMatches()` - knows fuzzy matching domain logic -- `findOrphans()` - knows what orphan means -- Domain assertions - know validation rules - -`RiviereBuilder` orchestrates these domain operations. - -### 5. Verify states are modeled as distinct types where appropriate - -**Status:** PASS - -Graph states distinguished: -- `BuilderGraph` (internal) - mutable, under construction -- `RiviereGraph` (output) - validated, final - -Component types modeled distinctly in schema package. - -### 6. Verify hidden domain concepts are extracted and named explicitly - -**Status:** PASS (after refinements) - -Explicit concepts: -- `ComponentId` value object (from schema) used for identity -- `NearMatchResult` explicitly models fuzzy match with score and mismatch info -- `BuilderWarning` explicitly models non-fatal issues with code - -### 7. Verify aggregates are designed around invariants - -**Status:** PASS - -`RiviereBuilder` is the aggregate root for graph construction: -- Invariant: No duplicate component IDs - enforced by `registerComponent()` -- Invariant: All components reference valid domains - enforced by `validateDomainExists()` -- Invariant: All links reference valid sources - enforced by `link()` and `linkExternal()` -- External code cannot directly mutate internal state - -### 8. Verify values are extracted into value objects - -**Status:** PARTIAL PASS - -Value objects in use: -- `ComponentId` (from schema) - used in error recovery -- `SourceLocation` (from schema) - location information - -Potential value objects not yet extracted: -- Domain name (currently primitive string) -- Module name (currently primitive string) - -These could be value objects but primitives are acceptable for simple identifiers without behavior. - ---- - -## Key Changes from Original Design - -| Change | Rationale | Impact | -|--------|-----------|--------| -| Remove `save()` method | Domain isolation, browser compatibility | Breaking change for callers using `save()` | -| Introduce feature directories | Separation of concerns | Internal reorganization | -| Split types.ts | Cohesion, co-location | Import paths change | -| Split errors.ts | Cohesion, co-location | Import paths change | -| Extract generic algorithms to platform/ | Separation of feature-specific from shared | Internal reorganization | -| Use ComponentId internally | Value object pattern, explicit domain concept | Internal improvement | - ---- - -## Trade-offs - -### Benefits of Refined Design - -1. **Clear feature boundaries** - Easy to understand what each feature does -2. **Cohesive modules** - Types and errors co-located with usage -3. **Testable in isolation** - Features can be tested independently -4. **Browser compatible** - Removing filesystem dependency enables browser usage -5. **Domain isolation** - No infrastructure concerns in domain code - -### Costs of Refined Design - -1. **More files** - ~20 files vs current ~12 -2. **Deeper nesting** - `features/graph-construction/domain/` vs flat `src/` -3. **Migration effort** - All imports need updating -4. **Import verbosity** - Longer import paths internally - -### Recommendation - -For a package of this size (~600 lines of production code), the refined structure provides meaningful benefits for maintainability and testability. The increased file count is offset by: -- Each file having single responsibility -- Types co-located with usage (no hunting through types.ts) -- Errors co-located with throwing code -- Clear boundaries for future growth - -The `save()` removal is the most impactful change, as it affects the public API. This should be considered carefully against the browser-compatibility benefit. - ---- - -## Dependency Flow - -```text -index.ts (public API) - │ - └── features/graph-construction/use-cases/riviere-builder.ts - │ - ├── features/graph-construction/domain/* - ├── features/graph-enrichment/domain/* - ├── features/graph-inspection/domain/* - ├── features/error-recovery/domain/* - │ - └── platform/domain/* -``` - -Rules: -- `use-cases/` depends on `domain/` (same feature or platform) -- `domain/` never depends on `use-cases/` -- Features do not depend on each other's `domain/` directly -- All cross-feature coordination goes through `RiviereBuilder` (the orchestrator) diff --git a/docs/design-reviews/riviere-builder/refinements.md b/docs/design-reviews/riviere-builder/refinements.md deleted file mode 100644 index df6f3591e..000000000 --- a/docs/design-reviews/riviere-builder/refinements.md +++ /dev/null @@ -1,194 +0,0 @@ -# Refinements: riviere-builder - -This document catalogs refinements to the original design based on the `separation-of-concerns` and `tactical-ddd` skills. - ---- - -## Separation of Concerns Refinements - -### SOC-1: Feature Directory Structure - -**Original:** Flat `src/` structure with all files at root level. - -**Refinement:** Introduce `features/`, `platform/`, and `shell/` directories. However, for this package, the structure requires adjustment: - -- This package is a **library**, not an application -- There are no external entrypoints (HTTP, CLI) - the `RiviereBuilder` class IS the API -- The `shell/` directory concept applies to wiring for applications, not libraries - -**Refined structure:** For libraries, the three-folder pattern adapts: -- `features/` contains feature-specific domain logic -- `platform/` contains shared capabilities -- The public API surface (`index.ts`) serves as the "shell" for libraries - -### SOC-2: Generic Utilities Extraction - -**Original:** `string-similarity.ts` and `deduplicateStrings()` in `deduplicate.ts` are in the feature code. - -**Refinement:** Extract generic algorithms to `platform/domain/`: -- `platform/domain/text-similarity/levenshtein.ts` - pure algorithm -- `platform/domain/collection-utils/deduplicate-strings.ts` - generic dedup - -The domain-specific `deduplicateStateTransitions()` stays with enrichment feature. - -### SOC-3: Split Types Spanning Multiple Capabilities - -**Original:** `types.ts` contains 18 interfaces spanning construction, enrichment, inspection, and near-matching. - -**Refinement:** Co-locate types with their features: -- `features/graph-construction/domain/input-types.ts` - component inputs -- `features/graph-enrichment/domain/enrichment-types.ts` - enrichment input -- `features/graph-inspection/domain/stats-types.ts` - stats and warnings -- `features/error-recovery/domain/match-types.ts` - near-match query/result - -### SOC-4: Split Errors Spanning Multiple Capabilities - -**Original:** `errors.ts` contains 12 error classes for domains, components, validation, custom types, and I/O. - -**Refinement:** Co-locate errors with their features: -- Construction errors: `DuplicateDomainError`, `DomainNotFoundError`, `DuplicateComponentError`, `CustomTypeNotFoundError`, `CustomTypeAlreadyDefinedError`, `MissingRequiredPropertiesError`, `MissingSourcesError`, `MissingDomainsError` -- Component lookup errors: `ComponentNotFoundError` -- Enrichment errors: `InvalidEnrichmentTargetError` -- Build/validation errors: `BuildValidationError`, `InvalidGraphError` -- I/O errors: `DirectoryNotFoundError` - -### SOC-5: Decompose builder-internals.ts - -**Original:** Mixes ID generation, error creation, and validation delegation in one file. - -**Refinement:** Split by responsibility: -- ID generation belongs with graph construction domain -- Validation delegation is unnecessary indirection - call assertions directly -- Error creation using suggestions belongs with error-recovery feature - -### SOC-6: Entrypoint vs Domain Violation - -**Original:** `RiviereBuilder` class mixes: -- Public API surface (entrypoint concern) -- Orchestration logic (use-case concern) -- Some domain decisions embedded in methods - -**Refinement:** For a builder pattern library, the class IS the public API. The current design is acceptable because: -- Methods are thin mappings to domain operations -- Validation is delegated to assertion functions -- Complex logic is extracted (e.g., `mergeBehavior`, `findNearMatches`) - -No structural change required, but document this as an intentional pattern for builder APIs. - ---- - -## Tactical DDD Refinements - -### DDD-1: Domain Isolation Violation - -**Original:** `save()` method in `RiviereBuilder` contains infrastructure code (filesystem access). - -**Refinement:** The `save()` method violates domain isolation by embedding `fs.access()` and `fs.writeFile()` directly. Options: -1. Remove `save()` from builder - callers use `build()` and handle I/O themselves -2. Accept a `Saver` interface and inject implementation (dependency inversion) - -For a library, option 1 is cleaner - the caller controls I/O. The `save()` method is a convenience that couples the library to Node.js. - -### DDD-2: Anemic Domain Model Detection - -**Original:** `enrichComponent()` method contains business logic: -```typescript -if (component.type !== 'DomainOp') { - throw new InvalidEnrichmentTargetError(id, component.type) -} -``` - -**Refinement:** This is acceptable orchestration-level validation (checking a precondition before operating). The actual domain logic is in `mergeBehavior()` and the deduplication functions. The model is not anemic because: -- `mergeBehavior()` contains business rules about merging -- `deduplicateStateTransitions()` knows what makes transitions equal -- The builder orchestrates these domain operations - -### DDD-3: Missing Value Objects - -**Original:** Component ID is a primitive string passed around. The schema has `ComponentId` but the builder uses raw strings. - -**Refinement:** The `ComponentId` value object from `@living-architecture/riviere-schema` should be used internally: -- `generateComponentId()` should return `ComponentId`, not `string` -- Methods accepting IDs could accept `ComponentId | string` for convenience -- Internal storage could use `ComponentId` to enforce validation at boundaries - -This would make the domain concept of "component identity" explicit throughout. - -### DDD-4: Missing Value Objects - StateTransition Equality - -**Original:** `deduplicateStateTransitions()` implements equality inline: -```typescript -e.from === item.from && e.to === item.to && e.trigger === item.trigger -``` - -**Refinement:** `StateTransition` should be a value object with an `equals()` method. Since this type comes from `riviere-schema`, this refinement would need to be applied there. For now, the domain logic is correctly isolated in the deduplication function. - -### DDD-5: Rich Domain Language Audit - -**Original terms with potential improvements:** - -| Current | Issue | Suggested | -|---------|-------|-----------| -| `BuilderOptions` | Generic "options" | `GraphInitialization` or keep (acceptable for builder pattern) | -| `enrichComponent()` | Verb is good | Keep | -| `nearMatches()` | Good domain term | Keep | -| `registerComponent()` | Implementation detail | `addToGraph()` or keep as private | -| `BuilderGraph` | Internal type | `GraphUnderConstruction` to indicate mutable state | -| `InspectionGraph` | Parameter type for inspection | `GraphSnapshot` or `GraphForInspection` | - -Most terms are acceptable. The "Builder" prefix consistently indicates the construction phase. - -### DDD-6: Aggregate Boundary Analysis - -**Original:** `RiviereBuilder` treats `graph.components[]` as mutable array accessible from multiple methods. - -**Refinement:** The `RiviereBuilder` class IS the aggregate root for graph construction. It correctly: -- Controls all additions via methods (no direct array manipulation from outside) -- Enforces uniqueness invariant via `registerComponent()` -- Validates domain existence before creating components - -The aggregate boundary is sound. The internal `graph` state is not exposed for direct mutation. - -### DDD-7: Make Implicit Explicit - Graph States - -**Original:** A graph under construction vs a validated graph are the same type with different guarantees. - -**Refinement:** The design already distinguishes: -- `BuilderGraph` - internal, mutable, may be invalid -- `RiviereGraph` - output of `build()`, guaranteed valid - -This could be made even more explicit with union types: -```typescript -type GraphState = - | DraftGraph // Under construction, mutable - | ValidatedGraph // Passed validation, immutable -``` - -But the current design with `build()` returning `RiviereGraph` provides sufficient clarity. - -### DDD-8: Separate Generic Concepts - -**Original:** `levenshteinDistance()` and `similarityScore()` are generic text algorithms. - -**Refinement:** These are correctly identified as generic in the SoC analysis. They should move to `platform/domain/text-similarity/`. The domain-specific usage (component name matching) stays in `features/error-recovery/`. - ---- - -## Summary of Refinements - -| ID | Category | Severity | Description | -|----|----------|----------|-------------| -| SOC-1 | Structure | High | Introduce features/platform directories for library organization | -| SOC-2 | Structure | Medium | Extract generic utilities to platform/domain | -| SOC-3 | Cohesion | Medium | Split types.ts by feature | -| SOC-4 | Cohesion | Medium | Split errors.ts by feature | -| SOC-5 | Cohesion | Low | Decompose builder-internals.ts | -| SOC-6 | Structure | Info | Document builder pattern as intentional entrypoint design | -| DDD-1 | Isolation | Medium | Remove or inject I/O in save() method | -| DDD-2 | Anemia | Info | Model is not anemic - validation confirmed | -| DDD-3 | Value Object | Medium | Use ComponentId value object internally | -| DDD-4 | Value Object | Low | StateTransition equality (schema package concern) | -| DDD-5 | Language | Low | Minor naming improvements possible | -| DDD-6 | Aggregate | Info | Aggregate boundary is sound | -| DDD-7 | Explicitness | Low | Graph states adequately distinguished | -| DDD-8 | Generic | Medium | Confirmed need to extract text-similarity algorithms | diff --git a/docs/design-reviews/riviere-cli/critique.md b/docs/design-reviews/riviere-cli/critique.md index 5193f5aad..c0ea06efc 100644 --- a/docs/design-reviews/riviere-cli/critique.md +++ b/docs/design-reviews/riviere-cli/critique.md @@ -19,7 +19,7 @@ Reviewed: docs/design-reviews/riviere-cli/refined.md ### Entrypoint Imports Domain Directly via parseTypeSpecificInput - **What's wrong:** The entrypoint example in section 1 (line 166 of refined.md) shows `parseTypeSpecificInput(options)` which suggests the entrypoint is aware of component type variations. The entrypoint is doing domain-level parsing decisions, not just mapping CLI strings to a command object. -- **Why it matters:** Violates separation of concerns checklist item 14: "entrypoint/ is thin (parse input -> invoke use-case -> map output) and never imports from domain/". The entrypoint should not know about component type semantics. +- **Why it matters:** Violates [ADR-002](../../architecture/adr/ADR-002-allowed-folder-structures.md): entrypoints validate primitive input shape and never import from `domain/`. The entrypoint should not know about component type semantics. - **Suggested fix:** Move type-specific input parsing to the use case. Entrypoint should pass raw options; use case or domain decides how to interpret them. ### Value Object SourceLocation Uses Primitive for filePath @@ -39,7 +39,7 @@ Reviewed: docs/design-reviews/riviere-cli/refined.md ### Generic Type-Grouping File: platform/domain/value-objects/ - **What's wrong:** The design places all value objects in `platform/domain/value-objects/` as a flat folder. This is a generic type-grouping approach (collecting all things of type "value object" together). -- **Why it matters:** Violates separation of concerns checklist item 13: "Verify no generic type-grouping files (types.ts, errors.ts, validators.ts) spanning multiple capabilities." Value objects should be co-located with the domain concepts they represent or grouped by domain concept, not by "being a value object." +- **Why it matters:** Violates the local architecture rule against generic type-grouping files spanning multiple capabilities. Value objects should be co-located with the domain concepts they represent or grouped by domain concept, not by "being a value object." - **Suggested fix:** Group value objects by the domain concept they belong to. E.g., `platform/domain/architectural-classification/component-type.ts` is good. `platform/domain/source-tracking/source-location.ts`, `platform/domain/source-tracking/repository-url.ts`, `platform/domain/source-tracking/file-path.ts` would be better than a flat `value-objects/` folder. ### Unclear Boundary: module-ref-resolver in extract-architecture/domain diff --git a/docs/design-reviews/riviere-cli/refinements.md b/docs/design-reviews/riviere-cli/refinements.md index 4f7c4e384..1cc6e3197 100644 --- a/docs/design-reviews/riviere-cli/refinements.md +++ b/docs/design-reviews/riviere-cli/refinements.md @@ -1,6 +1,6 @@ # riviere-cli Design Refinements -This document captures the refinements applied to the Architect's design using the `separation-of-concerns` and `tactical-ddd` skills. +This document captures the refinements applied to the Architect's design using the repository's local architecture and tactical DDD rules. --- @@ -8,7 +8,7 @@ This document captures the refinements applied to the Architect's design using t ### 1. Feature Identification -The Architect identified three features based on command groups (build-graph, extract-components, query-graph). This aligns with the separation-of-concerns principle of verticals, but the feature names could better reflect user goals. +The Architect identified three features based on command groups (build-graph, extract-components, query-graph). This aligns with [ADR-002](../../architecture/adr/ADR-002-allowed-folder-structures.md), but the feature names could better reflect user goals. **Refinement:** Rename features to reflect user intentions rather than technical operations: diff --git a/docs/design-reviews/riviere-extract-ts/critique.md b/docs/design-reviews/riviere-extract-ts/critique.md index 3346c7e06..1c8f8a79e 100644 --- a/docs/design-reviews/riviere-extract-ts/critique.md +++ b/docs/design-reviews/riviere-extract-ts/critique.md @@ -7,7 +7,7 @@ Reviewed: docs/design-reviews/riviere-extract-ts/refined.md ### Missing use-cases/ layer violates Separation of Concerns architecture - **What's wrong:** The refined design has `features/component-extraction/entrypoint/` going directly to `domain/`. There is no `use-cases/` layer. Every feature should have `entrypoint/`, `use-cases/`, and `domain/`. The design claims "entrypoint/ only where needed" but this contradicts the mandatory three-layer structure. -- **Why it matters:** Without use-cases/, orchestration logic will leak into either entrypoint or domain. The entrypoint becomes fat (parsing + orchestration + output mapping) or domain becomes polluted with workflow concerns. This is exactly what the Separation of Concerns skill prohibits. +- **Why it matters:** Without use-cases/, orchestration logic will leak into either entrypoint or domain. The entrypoint becomes fat (parsing + orchestration + output mapping) or domain becomes polluted with workflow concerns. This violates the dependency direction defined in ADR-002. - **Suggested fix:** Add `use-cases/` to each feature. Move orchestration from `extractDraftComponents` into a use case. The entrypoint should only parse input and invoke the use case. ### predicate-matching and value-extraction features have no entrypoint/ diff --git a/docs/design-reviews/riviere-extract-ts/design.md b/docs/design-reviews/riviere-extract-ts/design.md index 27bc20d51..0d6b1ae74 100644 --- a/docs/design-reviews/riviere-extract-ts/design.md +++ b/docs/design-reviews/riviere-extract-ts/design.md @@ -1,4 +1,4 @@ -# Separation of Concerns Analysis: riviere-extract-ts +# Architecture Analysis: riviere-extract-ts ## Package Overview diff --git a/docs/design-reviews/riviere-query/refined.md b/docs/design-reviews/riviere-query/refined.md index 03ec199ef..9ccf8abaa 100644 --- a/docs/design-reviews/riviere-query/refined.md +++ b/docs/design-reviews/riviere-query/refined.md @@ -80,7 +80,7 @@ packages/riviere-query/test/ riviere-graph-fixtures.ts # Test fixtures ``` -## Separation of Concerns Analysis +## Responsibility Analysis ### Principle 1: Separate external clients from domain-specific code diff --git a/docs/design-reviews/riviere-query/refinements.md b/docs/design-reviews/riviere-query/refinements.md index 92f57ed72..2aa120ff1 100644 --- a/docs/design-reviews/riviere-query/refinements.md +++ b/docs/design-reviews/riviere-query/refinements.md @@ -1,6 +1,6 @@ # Refinements: riviere-query -This document captures the refinements applied to the original design review using separation-of-concerns and tactical-ddd principles. +This document captures the refinements applied to the original design review using the repository's local architecture rules and role definitions. ## Separation of Concerns Refinements diff --git a/docs/project/PRD/active/PRD-phase-13-extraction-workflows.md b/docs/project/PRD/active/PRD-phase-13-extraction-workflows.md index bf668d004..8c7a96cdd 100644 --- a/docs/project/PRD/active/PRD-phase-13-extraction-workflows.md +++ b/docs/project/PRD/active/PRD-phase-13-extraction-workflows.md @@ -102,7 +102,7 @@ Phase 13 ships built-in step types only. User plugin loading is out of scope, bu No step can read another step's config or mutate another step's private state. Cross-step diagnostics are explicit runtime state exposed only through the diagnostics contract — not hidden coupling. -**Adapter isolation rule:** Built-in step handlers may depend on workflow-owned interfaces and adapters only. Direct imports of `@eventcatalog/sdk`, `@asyncapi/parser`, or Node `child_process` are confined to `riviere-workflow`'s `platform/infra/external-clients` layer. Step handlers and registry/runtime code depend on those interfaces, not on vendor SDK/parser/process APIs directly. This keeps orchestration code thin and matches ADR-002's external-client boundary. +**Adapter isolation rule:** Built-in step handlers may depend on workflow-owned interfaces and adapters only. Direct imports of `@eventcatalog/sdk`, `@asyncapi/parser`, or Node `child_process` are confined to the workflow use-case package's `infra/external-clients` location. Step handlers and registry/runtime code depend on those interfaces, not on vendor SDK/parser/process APIs directly. This keeps orchestration code thin and matches ADR-002's external-client boundary. --- @@ -845,7 +845,7 @@ Phase 13 does **not** introduce new read-method names for step authors. The work | `builder.warnings()` | Non-fatal issues on the current graph (runtime logs the per-step delta). | | `builder.stats()` | Counts of components, links, domains. | | `builder.orphans()` | Component IDs with no incoming or outgoing links. | -| `builder.query(): RiviereQuery` | Full read-only query object (see `@living-architecture/riviere-query`). | +| `builder.query(): RiviereQuery` | Full read-only query object from `@living-architecture/riviere-builder-domain-model`. | | `builder.build(): RiviereGraph` | **Workflow-facade finalization**: underlying builder `build()` plus unresolved-workflow-diagnostic guard. Used for final output. | `RiviereQuery` already exposes `components()`, `links()`, `find(predicate)`, `findAll(predicate)`, `componentById(id)`, `componentsInDomain(name)`, `componentsByType(type)`, `publishedEvents()`, `eventHandlers()`, `externalLinks()`, and more. Phase 13 does **not** add draft-only helper methods to `RiviereQuery`. AI steps that need incomplete-state information read it from `StepContext.diagnostics`, not from `RiviereGraph`. @@ -979,11 +979,11 @@ riviere-workflow **Repository hygiene requirements for the new `riviere-workflow` package:** -- **Folder structure** follows the monorepo convention for library packages (`src/features/*`, `src/platform/*`, `src/index.ts`) per ADR-002 and the `separation-of-concerns` skill. No `src/shell/*` is expected unless the package later grows true app-wiring concerns. -- **Dependency-cruiser rules** copied and adapted from existing packages so no cross-feature imports, no domain-to-upward dependencies, and `entrypoint` restrictions are enforced from day one. Added to the repo's root `dependency-cruiser.mjs`. +- **Folder structure** follows the monorepo convention for library packages (`src/features/*`, `src/platform/*`, `src/index.ts`) per ADR-002 and `.riviere/role-enforcement.config.ts`. No `src/shell/*` is expected unless the package later grows true app-wiring concerns. +- **Location and dependency rules** added to `.riviere/role-enforcement.config.ts` so feature isolation, domain and infrastructure boundaries, adapter restrictions, folder structure, and circular-import checks apply from day one. - **Role enforcement:** `riviere-workflow` is enforced per `.riviere/role-enforcement.config.ts`. Every exported declaration in the package receives a `/** @riviere-role */` tag. Roles for the new package (e.g. `workflow-runtime`, `step-handler`, `step-registry`) are added to `.riviere/roles.ts` in the same PR that introduces the package. - **Coverage:** 100% test coverage mandatory per the root `CLAUDE.md` testing convention. -- **Cross-package imports:** uses `@living-architecture/riviere-builder`, `@living-architecture/riviere-extract-config`, `@living-architecture/riviere-extract-ts`, `@living-architecture/riviere-schema`, `@living-architecture/riviere-query` via workspace references; never relative paths across package boundaries. +- **Cross-package imports:** uses `@living-architecture/riviere-builder-domain-model`, `@living-architecture/riviere-extract-config-published-language`, `@living-architecture/riviere-extract-ts-domain-model`, and `@living-architecture/riviere-schema-published-language` via workspace references; never relative paths across package boundaries. ### 3.7.1 Milestones @@ -1642,7 +1642,7 @@ Phase 13 is intentionally narrow. The exclusions below centralize the scope boun | 9 | `riviere workflow init` produces valid workflow YAML and step configs | Init creates files, `workflow validate` passes, `workflow run` succeeds | | 10 | `riviere workflow validate` catches invalid workflow files, missing config references, incompatible step-declared domains/sources, invalid step configs, and unresolved runtime prerequisites from `requiredServices()` (notably: the AI CLI executable not being in `PATH`); `workflow run` skips AI file-existence checks, AI config validation, and AI prerequisite checks for `--skip-ai`, and skips AI prerequisite checks for `--dry-run` steps that will not invoke a CLI | Unit tests for structural, semantic, and runtime-prerequisite validation; explicit tests that `workflow validate` fails on a non-existent AI CLI executable while `workflow run --skip-ai` ignores AI config/prerequisite failures and `--dry-run` does not require the AI CLI executable | | 11 | Workflow JSON Schema validates workflow file structure | Schema tests in `riviere-extract-config` accept documented valid examples and reject missing/invalid structural fields | -| 12 | `riviere-workflow` exports the step contract and resolves built-in steps through a registry rather than hardcoded switch logic | Unit tests for step registry + dependency-cruiser rule enforcement | +| 12 | `riviere-workflow` exports the step contract and resolves built-in steps through a registry rather than hardcoded switch logic | Unit tests for step registry plus role-enforcement location and dependency rules | | 13a | Workflows with **only deterministic steps** (no `ai-extract`, no `ai-enrich`) are bit-for-bit idempotent: running twice produces identical output JSON (after canonical-serialisation normalisation) | E2E test in CI: demo workflow with AI steps disabled, run twice, assert byte-equal output JSON. Mandatory gate. | | 13b | Workflows with AI steps are idempotent **only under pinned-runtime conditions** (pinned model/version, deterministic inference controls, replayable prompt inputs). Phase 13 does not ship pinned-runtime tooling; this criterion is explicitly deferred | Documented as not-in-scope; no CI gate. A manual verification procedure is published so teams with pinned runtimes can self-verify. | | 14 | AI step configs validate structured `command`/`args`, optional `memory` / `prompt-append`, and bounded enum-based selection and field lists rather than free-form strings | Schema validation tests in `riviere-extract-config` | @@ -1676,7 +1676,7 @@ Phase 13 is intentionally narrow. The exclusions below centralize the scope boun | 42 | Per-step transition fixtures in the demo-app repo are generated via the documented capture procedure (§3.8.3 fixture generation) by serialising `builder.query()` reads after each step — fixtures are never hand-edited | Demo-app repo includes the capture-hook tooling and a CI check that regenerating fixtures against a known-good run produces identical files | | 43 | Workflow `output` is required (no default); missing or empty `output` fails structural validation | Schema tests: workflow without `output` fails; `output: ""` fails; any non-empty string passes | | 44 | `ai-extract` source-scope overflow (files > `max-files-per-batch * max-batches`) fails the step with the documented error — silent truncation is disallowed | Integration test seeds a source tree with enough files to exceed the bound and asserts the step fails with the documented message | -| 45 | `riviere-workflow` package follows monorepo repository hygiene: separation-of-concerns folder structure, dependency-cruiser rules added to the root config, role-enforcement tags on every export, 100% test coverage, workspace-reference imports only, and explicit adapter isolation so only `platform/infra/external-clients` may import `@eventcatalog/sdk`, `@asyncapi/parser`, or Node `child_process` | Lint + dependency-cruiser + coverage gates green on CI, including rules that forbid vendor SDK/parser/process imports outside the adapter layer | +| 45 | The workflow packages follow monorepo repository hygiene: ADR-002 folder structure and dependency rules added to role enforcement, role tags on every export, 100% test coverage, workspace-reference imports only, and explicit adapter isolation so only `infra/external-clients` may import `@eventcatalog/sdk`, `@asyncapi/parser`, or Node `child_process` | Role enforcement, lint, and coverage gates green on CI, including location rules that forbid vendor SDK/parser/process imports outside generic external clients | | 46 | Architecture docs are updated for the workflow runtime boundary: `docs/architecture/overview.md` shows `riviere-workflow` in the package/dependency view, and a new ADR captures the registry runtime + shared-builder boundary | Doc diff assertions confirm `overview.md` includes `riviere-workflow`, and a new ADR file is added describing the runtime boundary and builder ownership | | 47 | Workflow/importer terminology and dependency docs are updated: the glossary includes workflow terms, and architecture docs mention `@eventcatalog/sdk` and `@asyncapi/parser` | Grep/doc assertions confirm glossary entries for `Workflow`, `Step Config`, `Mappings File`, and `Canonical Identity`, and confirm architecture docs mention both importer dependencies | | 48 | Operator-facing docs capture the AI CLI shell-out boundary and deferred AI idempotency expectations without implying an SDK/auth surface in Riviere | Grep/doc assertions confirm docs state `command` + `args`, `child_process.spawn`, no AI SDK/auth handling in Riviere, and manual-only AI idempotency guidance | @@ -1741,8 +1741,8 @@ The runtime can load a workflow, validate the active plan, and execute sequentia - Verification: doc diff assertions confirm the updated package diagram, presence of the ADR file, and the required glossary entries. - **D1.5:** Workflow schema and package foundations are strict by default - Key scenarios: workflow schema enforces `apiVersion`, required `output`, unique/patterned step names, and empty-string rejection; file-relative path resolution is reusable across built-in steps; `riviere-workflow` obeys repo hygiene rules. - - Acceptance criteria: schema tests cover structural validation rules; resolver tests prove file-relative path behaviour; dependency-cruiser, role-enforcement, coverage, and workspace-import gates apply to the new package; vendor SDK/parser/process imports are confined to `platform/infra/external-clients`. - - Verification: schema test suite, resolver unit tests, and CI lint/dependency-cruiser/coverage assertions, including import-boundary rules for the adapter layer. + - Acceptance criteria: schema tests cover structural validation rules; resolver tests prove file-relative path behaviour; role-enforcement, coverage, and workspace-import gates apply to the new packages; vendor SDK/parser/process imports are confined to `infra/external-clients`. + - Verification: schema test suite, resolver unit tests, and CI role-enforcement/lint/coverage assertions, including import-boundary rules for the adapter layer. ### M2: Deterministic spec and validation steps work end-to-end @@ -1996,16 +1996,16 @@ src/ **Import-boundary rules:** -- Only `platform/infra/external-clients/ai-cli-runner` may import Node `child_process`. -- Only `platform/infra/external-clients/eventcatalog-sdk-client` may import `@eventcatalog/sdk`. -- Only `platform/infra/external-clients/asyncapi-parser-client` may import `@asyncapi/parser`. +- Only `infra/external-clients/ai-cli-runner` may import Node `child_process`. +- Only `infra/external-clients/eventcatalog-sdk-client` may import `@eventcatalog/sdk`. +- Only `infra/external-clients/asyncapi-parser-client` may import `@asyncapi/parser`. - Step handlers, registry/runtime code, and CLI entrypoints depend on workflow-owned interfaces/adapters, not on vendor SDK/parser/process APIs directly. **Architecture alignment with existing docs:** - Aligns with `docs/architecture/overview.md` by keeping extraction, builder, schema, and query responsibilities separate and composable. - Aligns with ADR-001 by preserving extraction metadata logic in the extraction pipeline rather than moving extraction semantics into workflow glue. -- Aligns with ADR-002 by requiring `riviere-workflow` to follow the feature/platform/shell package structure and dependency-cruiser enforcement from day one. +- Aligns with ADR-002 by requiring `riviere-workflow` to follow the feature/platform/shell package structure and role-enforcement location rules from day one. **New dependencies and boundaries:** diff --git a/docs/project/PRD/archived/PRD-phase-12-connection-detection.md b/docs/project/PRD/archived/PRD-phase-12-connection-detection.md index 344aae7d1..7d18e346c 100644 --- a/docs/project/PRD/archived/PRD-phase-12-connection-detection.md +++ b/docs/project/PRD/archived/PRD-phase-12-connection-detection.md @@ -801,7 +801,7 @@ None. Connection detection uses ts-morph (already a dependency of `riviere-extra **9.1.1 No new packages — all changes modify existing packages** (Firm) -Connection detection uses the same ts-morph `Project`, same source files, same AST as component extraction. Same state dependencies → same module (separation-of-concerns principle 4). +Connection detection uses the same ts-morph `Project`, same source files, same AST as component extraction. The same state dependencies belong in the same module. | Package | Change | | ----------------------------- | ------------------------------------------------------------------------------------ | diff --git a/docs/project/PRD/notstarted/PRD-phase-14-cross-repo-linking.md b/docs/project/PRD/notstarted/PRD-phase-14-cross-repo-linking.md index 0db52c3fd..e569f75d6 100644 --- a/docs/project/PRD/notstarted/PRD-phase-14-cross-repo-linking.md +++ b/docs/project/PRD/notstarted/PRD-phase-14-cross-repo-linking.md @@ -42,7 +42,7 @@ builder.linkExternal({ ### Merge Algorithm ```typescript -import { mergeGraphs } from '@living-architecture/riviere-builder'; +import { mergeGraphs } from '@living-architecture/riviere-builder-domain-model'; const merged = mergeGraphs([ ordersGraph, diff --git a/docs/project/PRD/notstarted/prd-role-enforcement.md b/docs/project/PRD/notstarted/prd-role-enforcement.md deleted file mode 100644 index 4056f3e12..000000000 --- a/docs/project/PRD/notstarted/prd-role-enforcement.md +++ /dev/null @@ -1,387 +0,0 @@ -# PRD: Architecture Role Enforcement - -Keeping a codebase well organized is important for navigating the code and helping to keep the code well designed and maintainable. However, this is rarely the case because responsibilities get mixed and code gets added in different folders randomly. - -To solve this problem we are going to create software-role-dsl. The DSL defines a list of roles. Each class in code, or static function that is not part of a class, must have a role. And the role defines where the code should live. - -For example: - -1. `domain-service` => must live in /src/{feature}/domain/ -2. `cli-entrypiont` => must live in /src/{feature}/entrypoint/ - -Roles should be added as decorators like: - -```ts -@RiviereRole(RIVIERE_ROLE.AGGREGATE) -class Loan { - ... -} -``` - -Configuration should be as simple and minimal as possible: - -```json -{ - roles: [ - aggregate: { - allowedLocations: ['/src/{feature}/domain'] - } - ... - ] -} -``` - -## Objectives - -1. Build a system that allows roles to be configured and enforced -2. Tool should enforce 100% codebase compliance (every class or static method outside a class must have a role decorator) -3. Start by applying to the riviere codebase itself => 100% coverage (except schema packages) -4. In riviere our roles should be generic and reusable across codebases => roles like Aggregate are common industry terms and patterns so can be used. We should not have roles specific to our domain like 'connection-extractor' -5. Create specialist subagents that can be used to apply the rules to a given piece of code and correctly determine the role(s) of the code. It should suggest refactorings where needed, for example "this code combines domain-specific and generic logic, it should be decoupled into multiple roles and each one should be located in the relevant folder" - -### Implementation requirements - -1. Speed is crucial. Let's try oxlint => we need to include performance diagnostics -2. When there is an error the error should provide a clear diagnostic "{role} cannot live in {location}. See {config file for allowed roles}" -3. The config file should have a json schema and be validated before being parsed. -4. The riviere codebase may require refactoring and our architecture rules may need to evolve -5. new package in riviere riviere-role-enforcement => must follow all of our existing lint rules and 100% coverage - -## Plan - -We must work in small iterations. Initial we need to do discovery: - -1. What is our config format? -2. Do a very small POC based on some of our real code -3. Define our roles and rules - review code together to define rules and build our expert subagent that we can rly on later. Every new role added must be approved by a human user. -4. Go through the riviere codebase in iterations (one package at a time) and identify roles and required refactoring. - -Each iteration should be a separate PR and we want PRs to be as small as possible. - -## Phase 1 Plan - -Goal: complete the discovery and proof-of-concept iteration without inventing unapproved roles or baking Riviere-specific paths into the role model. - -### Scope - -Phase 1 applies only to one pilot slice: - -- `packages/riviere-cli/src/features/extract/**/*.ts` - -Phase 1 excludes: - -- `**/*.spec.ts` -- `**/__fixtures__/**` -- `.tsx` files -- schema packages -- repository-wide rollout -- decorators -- classifier subagents - -### Phase 1 Outputs - -Phase 1 must produce all of the following: - -1. A minimal `riviere-role-enforcement` package using `oxlint` -2. A minimal config format plus JSON schema validation -3. A proof-of-concept check over one real feature slice -4. A complete file-by-file role analysis for the pilot slice -5. A human-reviewed role catalog for the pilot slice before any enforcement is treated as final - -### Discovery Requirement - -Phase 1 is not allowed to invent a role list and silently proceed. The role catalog for the pilot slice must come from analysis and human review. - -Rules: - -1. Every new role must be approved by a human user -2. Roles must be generic and reusable across codebases -3. Roles must describe responsibility, not just folder placement -4. The pilot analysis must reach 100% coverage for the selected feature slice -5. If a file mixes responsibilities, the output should say so and recommend refactoring rather than forcing a weak role - -### Candidate Roles For Phase 1 Review - -Currently approved role names from review: - -- `cli-entrypoint` -- `command-use-case` -- `command-use-case-input` -- `command-use-case-result` -- `external-client-service` -- `cli-output-formatter` - -All other Phase 1 roles still require human naming approval. - -Important constraints for those names: - -- role names must be generic and reusable across codebases -- role names must not include Rivière-specific language -- role names must not include pilot-feature language from the current codebase -- role names must not include words taken from the current problem domain or current feature implementation -- role names must describe responsibility rather than just folder placement - -Examples of rejected naming patterns: - -- names derived from the current feature, such as `extraction-*`, `enrichment-*`, or `relationship-*` -- names derived from current package structure rather than responsibility -- names that only restate a folder name - -### File Groups Requiring Human Role Naming - -The following responsibility groups need approved generic role names before implementation: - -- `packages/riviere-cli/src/features/extract/commands/run-extraction.ts` - - approved role: `command-use-case` - - current responsibility: command-side workflow coordination - - review notes: - - command must take exactly one parameter - - that parameter should be named `runExtractionInput` - - that parameter type must have role `command-use-case-input` - - the `command-use-case-input` type must live in the `commands` folder - - command use case should follow `load -> invoke operation -> save/return` - - current implementation does too much and mixes orchestration, decision-making, and domain concerns - - `createModuleContexts(...)` is currently under `infra/external-clients` but appears domain-specific and should be reviewed - - choosing between `extractDraftComponents(...)` and `loadDraftComponentsFromFile(...)` mixes loading concerns with domain invocation - - `resolvedConfig` already being passed in suggests part of the loading step happens outside the command use case - - `enrichPerModule(...)` plus post-checking `failedFields` in the command indicates an anemic domain model and likely missing domain behavior - -- `packages/riviere-cli/src/features/extract/commands/run-extraction-input.ts` - - approved role: `command-use-case-input` - - planned responsibility: the single input contract for `runExtraction` - -- `packages/riviere-cli/src/features/extract/commands/run-extraction-result.ts` - - approved role: `command-use-case-result` - - planned responsibility: the only return contract for `runExtraction` - -### Approved Refactorings Identified During Role Review - -- `runExtraction` must be refactored to accept a single `runExtractionInput` parameter -- `runExtractionInput` must be introduced in the `commands` folder and classified as `command-use-case-input` -- `runExtraction` must return only a `command-use-case-result` -- `runExtractionResult` must be introduced in the `commands` folder and classified as `command-use-case-result` -- the CLI options object must not be passed into the command use case directly -- the entrypoint must translate CLI options into `runExtractionInput` before invoking the command use case - -### Detailed Review Notes From `runExtraction` - -- loading existing values and extracting new values are not interchangeable operations -- `loadDraftComponentsFromFile(...)` is reloading existing state -- `extractDraftComponents(...)` is a domain operation and must not be treated as equivalent to loading existing state -- grouping those two branches together only because they both produce `DraftComponent[]` is a design error -- the likely missing concept is an `Extraction` aggregate that owns the current working state for this workflow -- refined direction: the aggregate may be better modeled as `ExtractionProject`, since the current workflow already centers on a project-shaped unit of work -- an `aggregate-repository` may be needed to load that aggregate -- an `aggregate-repository` must return an aggregate -- therefore an `aggregate-repository` cannot return `ModuleContext` unless `ModuleContext` is itself proven to be an aggregate -- if a candidate repository return type is not an aggregate, that is a design signal that the real aggregate has not yet been identified -- in that situation, the model must be recomposed until the aggregate boundary is explicit -- `DraftComponent[]` is not the aggregate; it is only part of the aggregate state -- `loadDraftComponentsFromFile(...)` is therefore only a partial state loader, not a valid aggregate repository on its own -- partial state loaders may exist as internal implementation details, but they must sit behind an aggregate repository that assembles the full aggregate -- the aggregate repository should load the remaining state and combine all parts into the aggregate before returning it -- repository loading should happen in the use case, not in the entrypoint -- entrypoints must not depend on repositories or datastores -- only `command-use-case` and `query-use-case` may load or save through repositories/datastores -- repositories may depend on lower-level technical helpers to access filesystem, parser state, or other tooling -- those technical helpers are not themselves repositories; they are implementation details behind the repository boundary -- if domain state requires multiple partial loaders, the repository is responsible for coordinating them and returning the assembled aggregate -- `createModuleContexts(...)` currently mixes repository-style assembly with lower-level technical helper calls and should be split accordingly -- `loadExtractionProject(...)` is currently misnamed because it returns `ts-morph`'s `Project`, not the aggregate -- if `ExtractionProject` is the aggregate, then `loadExtractionProject(...)` should evolve into the aggregate repository and return `ExtractionProject` -- `createModuleContexts(...)` should then be absorbed into the aggregate repository as internal assembly logic rather than surviving as a public boundary -- repositories may take external library clients as dependencies when needed for loading and saving -- if an external dependency is injected directly into the repository, it does not necessarily need a local wrapper role in our codebase -- external-client-service wrappers are still valid when we want an explicit boundary, but they are not mandatory for every external library call -- `ModuleContext[]` is not an aggregate; at best it appears to be part of the aggregate state and currently has unclear ownership -- `createModuleContexts(...)` is therefore suspiciously split across layers: it may be building part of an aggregate while living in infrastructure -- `detectConnectionsPerModule(...)` exposes an implementation detail in the API; the domain concept should be connection detection on the aggregate rather than `per-module` plumbing -- once the aggregate is identified, behavior may belong either on the aggregate itself or on a `domain-service` operating on that aggregate -- current direction: start by modeling enrichment behavior as a method on the aggregate rather than as a free function over decomposed state -- current direction: `extractDraftComponents(...)` should also start as a method on the aggregate, likely `ExtractionProject.extractDraftComponents(...)`, rather than as a free function over decomposed state -- `ModuleContext` remains a warning sign and should not remain as an unowned bag of state passed between layers -- `extractDraftComponents(...)` currently looks like procedural plumbing around lower-level extraction behavior rather than a well-shaped boundary -- current direction: the aggregate should expose two public operations in sequence: `extractDraftComponents(...)` as the first step, then `enrichComponents(...)` as the second step -- lower-level deterministic extraction logic such as `extractComponents(...)` can remain beneath the aggregate boundary as internal domain logic or a domain service -- current direction: the current `runExtraction` flow likely hides two distinct command use cases rather than one -- entrypoints must not compose multiple use cases; if composition is needed, that is a design warning sign that the boundary is wrong -- likely split: - - `extract-draft-components` as one `command-use-case` - - `enrich-draft-components` as a second `command-use-case` -- based on the current code, `detect-connections` is not yet a standalone `command-use-case` -- current code requires enrichment output before connection detection can run -- therefore connection detection currently belongs inside the second use case rather than as a third independently invokable use case -- each command use case may load the aggregate through a different repository method -- accepted CLI compromise for now: keep a single `extract` CLI command for user ergonomics -- the `extract` entrypoint may choose which single `command-use-case` to invoke based on validated flags -- this is an intentional UX trade-off rather than the ideal architectural shape -- even with one CLI command, each invocation should still dispatch to exactly one `command-use-case` -- likely repository-level distinction: - - load from source/project state for fresh draft extraction - - load from persisted draft state for enrichment/resume flows -- this better preserves the difference between computing draft state and reloading existing draft state - -### Role Enforcement Rules Confirmed During Review - -- `command-use-case` may accept only one parameter -- that parameter must have role `command-use-case-input` -- `command-use-case-input` must live in `/commands` -- `command-use-case` may return only `command-use-case-result` -- `command-use-case-result` must live in `/commands` -- the role-enforcement rule must support `allowedInputs` and `allowedOutputs` -- initial command rule configuration should enforce: - - `allowedInputs: ["command-use-case-input"]` - - `allowedOutputs: ["command-use-case-result"]` - -### External Client Rules Confirmed During Review - -- approved role: `external-client-service` -- generic location rule: `/infra/external-clients/{external-service-name}/{service}.ts` -- alternative acceptable structure: `/infra/external-clients/{external-service-name}/{service-name}.ts` -- for the current code, ts-morph-related technical helpers should be grouped under a `ts-morph` external service boundary -- current likely candidates for this role: - - `findModuleTsConfigDir(...)` - - `createConfiguredProject(...)` - - `loadExtractionProject(...)` -- these helpers should not be treated as repositories; they are technical services used behind a repository boundary - -### CLI Output Rules Confirmed During Review - -- approved role: `cli-output-formatter` -- generic location rule: `/infra/cli/output/{formatter}.ts` -- presenters in the CLI should use this role instead of a generic presenter role -- current likely candidate for this role: - - `packages/riviere-cli/src/features/extract/infra/mappers/present-extraction-result.ts` -- current refactoring notes: - - it should move out of `infra/mappers` - - it should live under the CLI output boundary - - it should not depend on the raw CLI options object - - it should format `command-use-case-result`, not a domain result type - -- `packages/riviere-cli/src/features/extract/domain/extract-draft-components.ts` - - responsibility: performs the first core transformation from analysis context to draft artifacts - -- `packages/riviere-cli/src/features/extract/domain/enrich-per-module.ts` - - responsibility: reconciles and upgrades intermediate artifacts while enforcing consistency - -- `packages/riviere-cli/src/features/extract/domain/detect-connections-per-module.ts` - - responsibility: infers relationships between already identified artifacts - -- `packages/riviere-cli/src/features/extract/domain/extraction-result.ts` - - responsibility: defines the stable result type returned by the feature workflow - -- `packages/riviere-cli/src/features/extract/infra/external-clients/create-module-contexts.ts` -- `packages/riviere-cli/src/features/extract/infra/external-clients/load-extraction-project.ts` -- `packages/riviere-cli/src/features/extract/infra/external-clients/create-configured-project.ts` -- `packages/riviere-cli/src/features/extract/infra/external-clients/find-module-tsconfig-dir.ts` - - responsibility: wrap technical integration with filesystem, tsconfig discovery, globbing, and static analysis tooling - -- `packages/riviere-cli/src/features/extract/infra/mappers/present-extraction-result.ts` - - responsibility: translate workflow results into user-facing output - -### Current Pilot Coverage State - -The pilot slice is fully enumerated. `command-use-case` is approved for `run-extraction.ts`. The remaining role names still require human approval. - -### Annotation Format For The POC - -For Phase 1 we use explicit comment annotations, not decorators: - -```ts -/** @riviere-role */ -export function example() { - ... -} -``` - -Supported in Phase 1: - -- top-level class declarations -- top-level function declarations -- top-level exported function expressions assigned to variables -- static methods on top-level classes - -Not supported in Phase 1: - -- nested functions -- non-exported internal callbacks -- `.tsx` files -- automatic fixes - -### Generic Config Shape - -The config format must stay minimal and generic. - -```yaml -include: - - - -ignorePatterns: - - '**/*.spec.ts' - - '**/__fixtures__/**' - -roles: - - name: - targets: [class, function, static-method] - allowedLocation: - - /src/{feature}/... -``` - -Important: - -- `allowedLocation` entries must be generic patterns -- generic patterns may use placeholders such as `/src/{feature}/...` -- the Rivière pilot may map those placeholders onto real paths, but the role definitions themselves must remain reusable - -### Phase 1 Pilot Workflow - -1. Analyze every non-test `.ts` file in `packages/riviere-cli/src/features/extract` -2. Produce a file-by-file classification table with proposed generic roles and rationale -3. Identify mixed-responsibility files and required refactors -4. Review and approve the role set with a human -5. Configure the POC with only approved roles -6. Annotate the pilot slice -7. Run `oxlint` and verify 100% compliance for the pilot slice - -### Enforcement Rules For The POC - -Once the pilot role catalog is approved, the POC enforces only these checks: - -1. Every supported in-scope symbol must have exactly one `@riviere-role` annotation -2. The assigned role must exist in config -3. The assigned role must allow the symbol kind -4. The assigned role must allow the file location - -Required diagnostic shape: - -- `{role} cannot live in {location}. See {config file}` - -Additional diagnostics may include: - -- missing role assignment -- unknown role -- malformed annotation -- duplicate annotation - -### Testing - -Phase 1 must include: - -- config validation tests -- annotation parsing tests -- target extraction tests -- role checking tests -- one integration test that runs the package entrypoint against fixture files -- performance diagnostics for the oxlint-based check - -### Success Criteria - -Phase 1 is complete when: - -1. `packages/riviere-role-enforcement` exists and is testable -2. the config file has schema validation -3. `oxlint` runs the custom enforcement rule successfully -4. the selected pilot slice has a reviewed file-by-file role analysis -5. the selected pilot slice reaches 100% compliance using only human-approved roles -6. diagnostics are clear enough for humans and AI tools to act on -7. the change remains a small, focused first PR diff --git a/docs/project/PRD/riviere-extraction-workflows-v1/ARCH.md b/docs/project/PRD/riviere-extraction-workflows-v1/ARCH.md index 39e3701a5..b4bed1960 100644 --- a/docs/project/PRD/riviere-extraction-workflows-v1/ARCH.md +++ b/docs/project/PRD/riviere-extraction-workflows-v1/ARCH.md @@ -26,7 +26,7 @@ The workflow must not simply wrap or chain existing CLI commands. Existing build The workflow feature should instead orchestrate workflow execution in memory and write the final graph only after all stages succeed. Existing lower-level Rivière capabilities such as deterministic extraction, graph building, linking, validation, and graph serialisation remain owned by their existing packages and command/use-case layers. -Concrete aggregate ownership: `RiviereProject` lives at `packages/riviere-extract-ts/src/features/extraction/domain/riviere-project.ts`, and `RiviereProjectRepository` lives at `packages/riviere-extract-ts/src/features/extraction/infra/persistence/riviere-project-repository.ts`. +Concrete aggregate ownership: `RiviereProject` lives at `packages/riviere-extract-ts/src/domain/riviere-project.ts`, and the single repository shared by the CLI's extract and workflow features lives at `packages/riviere-cli/src/data-access/riviere-project/riviere-project-repository.ts`. Important product boundary: workflows must not provide Rivière capabilities that the CLI does not provide. The CLI must not become “a watered down version of the full product.” Workflow execution may compose capabilities differently to protect all-or-nothing execution, but the underlying product capabilities should remain available through CLI surfaces rather than being hidden only inside workflow execution. @@ -35,7 +35,7 @@ Rejected ownership options: - A workflow wrapper around existing CLI commands was rejected because it would require graph state to be saved and reloaded between stages and would create cleanup complexity. - A new `packages/riviere-workflow` package was not selected for V1 because it adds package and API surface area before the first workflow slice is proven. It remains a possible future evolution if workflows need to be consumed outside the CLI. - `packages/riviere-builder` was rejected as the top-level workflow owner because workflow concerns include project-local workflow files, extraction config resolution, run logs, CLI progress, and future stage orchestration beyond pure graph building. -- `packages/riviere-extract-ts` was rejected as the top-level workflow feature owner because workflows include CLI workflow files, graph writing, run logs, and future non-deterministic AI-assisted stages. This does not move `RiviereProject` or `RiviereProjectRepository` out of the extraction package. +- `packages/riviere-extract-ts` was rejected as the top-level workflow feature owner because workflows include CLI workflow files, graph writing, run logs, and future non-deterministic AI-assisted stages. `RiviereProject` remains in that domain-model package; the CLI application's single repository for the aggregate lives in package-level `data-access/`, where both CLI features can use it. Future evolution notes: @@ -120,7 +120,7 @@ Option 1 is the accepted architecture direction. The approved reason is that the ##### Core idea -Introduce `RiviereProject` as the main aggregate for a Rivière project rooted in a repository. It lives at `packages/riviere-extract-ts/src/features/extraction/domain/riviere-project.ts`. `RiviereProjectRepository` lives at `packages/riviere-extract-ts/src/features/extraction/infra/persistence/riviere-project-repository.ts`. In this option, `ExtractionProject` is explicitly retired as an aggregate. Its current config/materialisation state and extraction behaviours move out of `riviere-cli` into the extraction domain package (`packages/riviere-extract-ts`). Workflow consumes that package-level extraction capability; it does not import `riviere-cli`'s `features/extract` and it does not own extraction. +Introduce `RiviereProject` as the main aggregate for a Rivière project rooted in a repository. It lives at `packages/riviere-extract-ts/src/domain/riviere-project.ts`. `RiviereProjectRepository` lives at `packages/riviere-cli/src/data-access/riviere-project/riviere-project-repository.ts`, where both CLI features can use the same aggregate repository without importing from one another. In this option, `ExtractionProject` is explicitly retired as an aggregate. Its extraction behaviour moves out of `riviere-cli` into the extraction domain package (`packages/riviere-extract-ts`), while the CLI package retains responsibility for loading that aggregate. Workflow consumes the package-level extraction domain capability; it does not import `riviere-cli`'s `features/extract` and it does not own extraction. The workflow file is one input used by `RiviereProjectRepository` to build project state for a selected workflow run. The project is identified by `projectRoot`; the workflow is selected by `workflowName` inside that project. Inline extract and link config paths in the user-facing workflow file are resolved during project loading into explicit executable stage state, not opaque configured steps and not nested aggregates. @@ -128,9 +128,9 @@ The workflow file is one input used by `RiviereProjectRepository` to build proje The current `ExtractionProject` abstraction is challenged directly and resolved in this option: it must not remain an aggregate. Keeping it as an aggregate is Option 2, not an unresolved choice inside this option. -This is also the target architecture for the existing extract feature. Current extract commands are rewired directly from `ExtractionProjectRepository`/`ExtractionProject` to `RiviereProjectRepository`/`RiviereProject` plus package-owned extraction domain services in `@living-architecture/riviere-extract-ts`. The CLI package remains responsible for CLI input, output, command use cases, workflow loading, and shell wiring; it does not own core extraction domain logic. If the team wants to keep the current extract command wiring while adding workflows, that is not Option 1; it is a different option with explicit architecture debt. +This is also the target architecture for the existing extract feature. Current extract commands are rewired directly from `ExtractionProjectRepository`/`ExtractionProject` to `RiviereProjectRepository`/`RiviereProject` plus package-owned extraction domain services in `@living-architecture/riviere-extract-ts-domain-model`. The CLI package remains responsible for CLI input, output, command use cases, workflow loading, and shell wiring; it does not own core extraction domain logic. If the team wants to keep the current extract command wiring while adding workflows, that is not Option 1; it is a different option with explicit architecture debt. -Non-CLI use note: this option makes the extraction project aggregate reusable outside the CLI because `RiviereProject` and `RiviereProjectRepository` live in `packages/riviere-extract-ts`. It does not, by itself, make the V1 workflow CLI surface reusable outside the CLI; command input, output, run-log writing, graph-file writing, and shell wiring remain under `packages/riviere-cli/src/features/workflow`. +Non-CLI use note: `RiviereProject` is reusable outside the CLI because it lives in `packages/riviere-extract-ts`. `RiviereProjectRepository` is the CLI application's repository and is shared by its extract and workflow features. Another application using the aggregate supplies its own repository. This option does not make the V1 workflow CLI surface reusable outside the CLI; command input, output, run-log writing, graph-file writing, and shell wiring remain under `packages/riviere-cli/src/features/workflow`. Workflow definitions use this V1 location and shape: @@ -208,7 +208,7 @@ flowchart LR entrypoint["createWorkflowRunCommand
(entrypoint)"] inputFactory["createRunWorkflowInput
(commands)"] useCase["RunWorkflow
(commands)"] - repository["RiviereProjectRepository
(riviere-extract-ts infra/persistence)"] + repository["RiviereProjectRepository
(riviere-cli package data-access)"] extractionStage["ExtractionStage
(riviere-extract-ts domain)"] extractComponents["ExtractComponentsForGraph
(riviere-extract-ts domain)"] detectConnections["DetectExtractionConnections
(riviere-extract-ts domain)"] @@ -256,17 +256,17 @@ Legend: gray = existing, yellow = changed, green = new, blue = explicit role/con | Component | Layer / path | Status | .riviere role | Responsibilities | Estimated size | | ----------------------------- | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------- | -| `createWorkflowRunCommand` | `packages/riviere-cli/src/features/workflow/entrypoint/run-workflow.ts` | New | `cli-entrypoint` | Define the workflow CLI command, call input factory, use case, and formatter. | Small | +| `createWorkflowRunCommand` | `packages/riviere-cli/src/features/workflow/entrypoint/run-workflow/entrypoint.ts` | New | `cli-entrypoint` | Define the workflow CLI command, call input factory, use case, and formatter. | Small | | `createRunWorkflowInput` | `packages/riviere-cli/src/features/workflow/commands/create-run-workflow-input.ts` | New | `command-input-factory` | Convert a CLI-neutral parsed-options shape into typed workflow input without reading files. Must not depend on Commander/raw CLI option types directly. | Small | | `RunWorkflow` | `packages/riviere-cli/src/features/workflow/commands/run-workflow.ts` | New | `command-use-case` | Load `RiviereProject` for `{ projectRoot, workflowName }`, call `rebuildGraph()`, return result. No stage loop, no builder construction, no graph/log file writing. | Small | | `RunWorkflowInput` | `packages/riviere-cli/src/features/workflow/commands/run-workflow-input.ts` | New | `command-use-case-input` | Project root, workflow name, and CLI output options. | Small | | `RunWorkflowResult` | `packages/riviere-cli/src/features/workflow/commands/run-workflow-result.ts` | New | `command-use-case-result` | Graph build success/failure, graph artefact, NDJSON run log events, run log path, and failure detail. | Small | -| `RiviereProjectRepository` | `packages/riviere-extract-ts/src/features/extraction/infra/persistence/riviere-project-repository.ts` | New | `aggregate-repository` | Load the full `RiviereProject` aggregate state for `{ projectRoot, workflowName }` or `{ projectRoot, configPath, useTsConfig }`; read workflow/config files, resolve graph metadata where present, load extract and link config/source state, materialise `ExtractionStage` value objects for extraction and link detection, and create the aggregate. Does not run stages. | Large | -| `RiviereProject` | `packages/riviere-extract-ts/src/features/extraction/domain/riviere-project.ts` | New | `aggregate` | Own ordered graph-building journey, empty-start rebuild invariant, fail-fast execution, run events, graph build result, and extract-command operations that replace `ExtractionProject`. Implementation requires adding `RiviereProject` to approved aggregate instances. | Medium | -| `ExtractionProject` | `packages/riviere-cli/src/features/extract/domain/extraction-project.ts` | Removed / replaced in Option 1 | none in target | No longer the extract aggregate or command-facing extraction object. Current extract command dependencies migrate to `@living-architecture/riviere-extract-ts` stage materialisation and extraction services. | Large | -| `ExtractionStage` | `packages/riviere-extract-ts/src/features/extraction/domain/extraction-stage.ts` | Changed / extracted from current `ExtractionProject` | `value-object` | Hold module contexts, resolved extraction config, repository name, and source/project context for one extraction config. Shared by CLI extract commands, CLI workflows, and future non-CLI consumers through the package API. | Large | -| `ExtractComponentsForGraph` | `packages/riviere-extract-ts/src/features/extraction/domain/extract-components-for-graph.ts` | New / extracted from current `ExtractionProject` | `domain-service` | Extract draft components and enrich them into graph-ready components without connection detection. Exported from `@living-architecture/riviere-extract-ts`. | Medium | -| `DetectExtractionConnections` | `packages/riviere-extract-ts/src/features/extraction/domain/detect-extraction-connections.ts` | New / extracted from current `ExtractionProject` | `domain-service` | Detect links from the resolved link-stage config/module contexts against accumulated graph-ready components. Exported from `@living-architecture/riviere-extract-ts`. | Medium | +| `RiviereProjectRepository` | `packages/riviere-cli/src/data-access/riviere-project/riviere-project-repository.ts` | New | `aggregate-repository` | Load the full `RiviereProject` aggregate state for `{ projectRoot, workflowName }` or `{ projectRoot, configPath, useTsConfig }`; read workflow/config files, resolve graph metadata where present, load extract and link config/source state, materialise `ExtractionStage` value objects for extraction and link detection, and create the aggregate. Does not run stages. | Large | +| `RiviereProject` | `packages/riviere-extract-ts/src/domain/riviere-project.ts` | New | `aggregate` | Own ordered graph-building journey, empty-start rebuild invariant, fail-fast execution, run events, graph build result, and extract-command operations that replace `ExtractionProject`. Implementation requires adding `RiviereProject` to approved aggregate instances. | Medium | +| `ExtractionProject` | `packages/riviere-cli/src/features/extract/domain/extraction-project.ts` | Removed / replaced in Option 1 | none in target | No longer the extract aggregate or command-facing extraction object. Current extract command dependencies migrate to `@living-architecture/riviere-extract-ts-domain-model` stage materialisation and extraction services. | Large | +| `ExtractionStage` | `packages/riviere-extract-ts/src/domain/extraction-stage.ts` | Changed / extracted from current `ExtractionProject` | `value-object` | Hold module contexts, resolved extraction config, repository name, and source/project context for one extraction config. Shared by CLI extract commands, CLI workflows, and future non-CLI consumers through the package API. | Large | +| `ExtractComponentsForGraph` | `packages/riviere-extract-ts/src/domain/extract-components-for-graph.ts` | New / extracted from current `ExtractionProject` | `domain-service` | Extract draft components and enrich them into graph-ready components without connection detection. Exported from `@living-architecture/riviere-extract-ts-domain-model`. | Medium | +| `DetectExtractionConnections` | `packages/riviere-extract-ts/src/domain/detect-extraction-connections.ts` | New / extracted from current `ExtractionProject` | `domain-service` | Detect links from the resolved link-stage config/module contexts against accumulated graph-ready components. Exported from `@living-architecture/riviere-extract-ts-domain-model`. | Medium | | `ApplyExtractionToGraph` | `packages/riviere-cli/src/features/workflow/domain/apply-extraction-to-graph.ts` | New | `domain-service` | Apply `riviere-extract-ts` output to concrete `RiviereBuilder` write methods as part of the workflow graph rebuild domain operation. This is not a generic mapper and not builder-owned: extraction owns extraction output, builder owns graph mutation rules, workflow owns applying extracted architecture into the rebuild journey. | Medium | | `RiviereBuilder` | `packages/riviere-builder/src/features/building/domain/builder-facade.ts` | Existing | `aggregate` | In-memory graph write abstraction only. Knows graph rules, not workflow/config/project setup. | Existing | | `presentWorkflowRunResult` | `packages/riviere-cli/src/features/workflow/entrypoint/run-workflow/present-workflow-run-result.ts` | New | `cli-output-formatter` / entrypoint-local output writer role from latest main | CLI boundary output handler. Writes the NDJSON run log and, on successful rebuild, writes the graph to the chosen output path using the command result plus CLI options. The accepted location follows the latest `entrypoint/{entrypoint}` structure from `main`, not the older feature-local `infra/cli/output` pattern. | Medium | @@ -363,7 +363,7 @@ ExtractComponentsForGraph.execute(extractionStage, { allowIncomplete: false }) DetectExtractionConnections.execute(extractionStage, allComponents, { allowIncomplete: false }) ``` -Role decision: `ExtractionStage` lives in `packages/riviere-extract-ts/src/features/extraction/domain` because extraction capability belongs to the extraction package, not the CLI package and not workflow. It is a `value-object`: data members only, branded, no public behaviour. Carrying `ts-morph` `Project` objects is accepted in this option as materialised analysis state; if implementation proves that incompatible with the existing value-object role, the role definition must be expanded deliberately rather than moving extraction state back into CLI. +Role decision: `ExtractionStage` lives in `packages/riviere-extract-ts/src/domain` because extraction capability belongs to the extraction domain model, not the CLI package and not workflow. It is a `value-object`: data members only, branded, no public behaviour. Carrying `ts-morph` `Project` objects is accepted in this option as materialised analysis state; if implementation proves that incompatible with the existing value-object role, the role definition must be expanded deliberately rather than moving extraction state back into CLI. ##### Runtime call outline @@ -534,25 +534,23 @@ export class RiviereProject { | Dependency | Status | Used by | Purpose | |---|---|---|---| | `RiviereBuilder` | Existing | `RiviereProject`, `ApplyExtractionToGraph` | Provides the empty in-memory graph write abstraction used during rebuild. | -| `@living-architecture/riviere-extract-ts` extraction-stage API | New / replaces current CLI `ExtractionProjectRepository` setup in Option 1 | `RiviereProjectRepository`, existing extract commands, future non-CLI consumers | Materialises extraction stages from real config paths, module contexts, resolved config, repository info, and ts-morph projects without creating `ExtractionProject` or depending on `riviere-cli`. | +| `@living-architecture/riviere-extract-ts-domain-model` extraction-stage API | New / replaces current CLI `ExtractionProjectRepository` setup in Option 1 | `RiviereProjectRepository`, existing extract commands, future non-CLI consumers | Materialises extraction stages from real config paths, module contexts, resolved config, repository info, and ts-morph projects without creating `ExtractionProject` or depending on `riviere-cli`. | ##### Code shape ```text -packages/riviere-cli/src/features/workflow/entrypoint/run-workflow.ts +packages/riviere-cli/src/features/workflow/entrypoint/run-workflow/entrypoint.ts packages/riviere-cli/src/features/workflow/commands/create-run-workflow-input.ts packages/riviere-cli/src/features/workflow/commands/run-workflow.ts packages/riviere-cli/src/features/workflow/commands/run-workflow-input.ts packages/riviere-cli/src/features/workflow/commands/run-workflow-result.ts -packages/riviere-extract-ts/src/features/extraction/infra/persistence/riviere-project-repository.ts -packages/riviere-cli/src/features/workflow/infra/persistence/workflow-run-log-writer.ts -packages/riviere-cli/src/features/workflow/infra/persistence/workflow-graph-output-writer.ts +packages/riviere-cli/src/data-access/riviere-project/riviere-project-repository.ts packages/riviere-cli/src/features/workflow/entrypoint/run-workflow/present-workflow-run-result.ts -packages/riviere-extract-ts/src/features/extraction/domain/riviere-project.ts +packages/riviere-extract-ts/src/domain/riviere-project.ts packages/riviere-cli/src/features/workflow/domain/apply-extraction-to-graph.ts -packages/riviere-extract-ts/src/features/extraction/domain/extraction-stage.ts -packages/riviere-extract-ts/src/features/extraction/domain/extract-components-for-graph.ts -packages/riviere-extract-ts/src/features/extraction/domain/detect-extraction-connections.ts +packages/riviere-extract-ts/src/domain/extraction-stage.ts +packages/riviere-extract-ts/src/domain/extract-components-for-graph.ts +packages/riviere-extract-ts/src/domain/detect-extraction-connections.ts ``` ##### Design validation @@ -569,7 +567,7 @@ Benefits: - Best alignment with “one project builds one graph”. - `project.rebuildGraph()` is a strong domain operation and enforces empty-start rebuild internally. - Avoids nested `ExtractionProject` aggregates because `ExtractionProject` is no longer an aggregate in this option. -- Keeps extraction capability owned by `@living-architecture/riviere-extract-ts`, so CLI extract commands, CLI workflow, and future non-CLI consumers compose the same core extraction behaviour. +- Keeps extraction capability owned by `@living-architecture/riviere-extract-ts-domain-model`, so CLI extract commands, CLI workflow, and future non-CLI consumers compose the same core extraction behaviour. - Keeps `RiviereBuilder` clean and decoupled. - Keeps user-facing workflow config simple while allowing a richer internal executable model. @@ -654,8 +652,8 @@ flowchart LR entrypoint["createWorkflowRunCommand
(entrypoint)"] inputFactory["createRunWorkflowInput
(commands)"] useCase["RunWorkflow
(commands)"] - contextRepository["RiviereProjectContextRepository
(infra/persistence)"] - extractionRepository["ExtractionProjectRepository
(infra/persistence)"] + contextRepository["RiviereProjectContextRepository
(data-access)"] + extractionRepository["ExtractionProjectRepository
(data-access)"] rebuilder["RiviereProjectGraphRebuilder
(domain/application)"] extractionProject["ExtractionProject
(domain)"] graphApplier["ApplyExtractionToGraph
(domain)"] @@ -696,14 +694,14 @@ Legend: gray = existing, yellow = changed, green = new, red = rejected design. | Component | Layer / path | Status | .riviere role | Responsibilities | Estimated size | | --------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -------- | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | -| `createWorkflowRunCommand` | `packages/riviere-cli/src/features/workflow/entrypoint/run-workflow.ts` | New | `cli-entrypoint` | Define the workflow CLI command, call input factory, use case, and formatter. | Small | +| `createWorkflowRunCommand` | `packages/riviere-cli/src/features/workflow/entrypoint/run-workflow/entrypoint.ts` | New | `cli-entrypoint` | Define the workflow CLI command, call input factory, use case, and formatter. | Small | | `createRunWorkflowInput` | `packages/riviere-cli/src/features/workflow/commands/create-run-workflow-input.ts` | New | `command-input-factory` | Convert CLI options into typed workflow input without reading files. | Small | | `RunWorkflow` | `packages/riviere-cli/src/features/workflow/commands/run-workflow.ts` | New | `command-use-case` | Load context, invoke one rebuilder, return result. No extraction loading and no stage loop. | Small | -| `RiviereProjectContextRepository` | `packages/riviere-cli/src/features/workflow/infra/persistence/riviere-project-context-repository.ts` | Rejected | repository | Read workflow file and resolve graph metadata plus ordered stage definitions. Does not load extraction projects or execute stages. | Medium | +| `RiviereProjectContextRepository` | `packages/riviere-cli/src/features/workflow/data-access/riviere-project-context-repository.ts` | Rejected | repository | Read workflow file and resolve graph metadata plus ordered stage definitions. Does not load extraction projects or execute stages. | Medium | | `RiviereProjectContext` | `packages/riviere-cli/src/features/workflow/domain/riviere-project-context.ts` | Rejected | value-object | Hold `BuilderOptions`, ordered stage definitions, extraction config paths, and stage names without owning aggregate state. | Medium | | `RiviereProjectGraphRebuilder` | `packages/riviere-cli/src/features/workflow/domain/riviere-project-graph-rebuilder.ts` | Rejected | domain-service | Own the explicit graph-state fold: create builder, load extraction project per extract stage, apply components, detect/apply links, validate, build result. | Large | | `ApplyExtractionToGraph` | `packages/riviere-cli/src/features/workflow/domain/apply-extraction-to-graph.ts` | New | `domain-service` | Apply `EnrichedComponent[]`, `ExtractedLink[]`, and `ExternalLink[]` onto real `RiviereBuilder` methods. | Medium | -| `ExtractionProjectRepository` | `packages/riviere-cli/src/features/extract/infra/persistence/extraction-project/extraction-project-repository.ts` | Existing | `aggregate-repository` | Load existing `ExtractionProject` aggregate from extraction config inputs. | Existing | +| `ExtractionProjectRepository` | `packages/riviere-cli/src/features/extract/data-access/extraction-project/extraction-project-repository.ts` | Existing | `aggregate-repository` | Load existing `ExtractionProject` aggregate from extraction config inputs. | Existing | | `ExtractionProject` | `packages/riviere-cli/src/features/extract/domain/extraction-project.ts` | Changed | `aggregate` | Continue to own configured extraction behaviour and expose graph-ready component extraction separately from connection detection. | Medium | | `RiviereBuilder` | `packages/riviere-builder/src/features/building/domain/builder-facade.ts` | Existing | `aggregate` | In-memory graph write abstraction only. | Existing | | `presentWorkflowRunResult` | `packages/riviere-cli/src/features/workflow/entrypoint/run-workflow/present-workflow-run-result.ts` | New | `cli-output-formatter` / entrypoint-local output writer role from latest main | Write graph/log to console or files according to CLI parameters. | Small | @@ -799,10 +797,10 @@ export class RiviereProjectGraphRebuilder { ##### Code shape ```text -packages/riviere-cli/src/features/workflow/entrypoint/run-workflow.ts +packages/riviere-cli/src/features/workflow/entrypoint/run-workflow/entrypoint.ts packages/riviere-cli/src/features/workflow/commands/create-run-workflow-input.ts packages/riviere-cli/src/features/workflow/commands/run-workflow.ts -packages/riviere-cli/src/features/workflow/infra/persistence/riviere-project-context-repository.ts +packages/riviere-cli/src/features/workflow/data-access/riviere-project-context-repository.ts packages/riviere-cli/src/features/workflow/domain/riviere-project-context.ts packages/riviere-cli/src/features/workflow/domain/riviere-project-graph-rebuilder.ts packages/riviere-cli/src/features/workflow/domain/apply-extraction-to-graph.ts @@ -859,9 +857,9 @@ flowchart LR entrypoint["createWorkflowRunCommand
(entrypoint)"] inputFactory["createRunWorkflowInput
(commands)"] useCase["RunWorkflow
(commands)"] - workflowRepository["WorkflowDefinitionRepository
(infra/persistence)"] + workflowRepository["WorkflowDefinitionRepository
(data-access)"] orchestrator["WorkflowGraphBuildOrchestrator
(application)"] - extractionRepository["ExtractionProjectRepository
(infra/persistence)"] + extractionRepository["ExtractionProjectRepository
(data-access)"] extractionProject["ExtractionProject
(domain)"] graphApplier["ApplyExtractionToGraph
(domain)"] builder["RiviereBuilder
(riviere-builder)"] @@ -901,14 +899,14 @@ Legend: gray = existing, yellow = changed, green = new, red = rejected design. | Component | Layer / path | Status | .riviere role | Responsibilities | Estimated size | |---|---|---|---|---|---| -| `createWorkflowRunCommand` | `packages/riviere-cli/src/features/workflow/entrypoint/run-workflow.ts` | New | `cli-entrypoint` | Define the workflow CLI command, call input factory, use case, and formatter. | Small | +| `createWorkflowRunCommand` | `packages/riviere-cli/src/features/workflow/entrypoint/run-workflow/entrypoint.ts` | New | `cli-entrypoint` | Define the workflow CLI command, call input factory, use case, and formatter. | Small | | `createRunWorkflowInput` | `packages/riviere-cli/src/features/workflow/commands/create-run-workflow-input.ts` | New | `command-input-factory` | Convert CLI options into typed workflow input without reading files. | Small | | `RunWorkflow` | `packages/riviere-cli/src/features/workflow/commands/run-workflow.ts` | New | `command-use-case` | Load workflow definition, invoke orchestrator, return result. No stage loop if possible. | Small | -| `WorkflowDefinitionRepository` | `packages/riviere-cli/src/features/workflow/infra/persistence/workflow-definition-repository.ts` | Rejected | repository | Read workflow file and resolve graph metadata plus ordered stage definitions. Does not execute stages. | Medium | +| `WorkflowDefinitionRepository` | `packages/riviere-cli/src/features/workflow/data-access/workflow-definition-repository.ts` | Rejected | repository | Read workflow file and resolve graph metadata plus ordered stage definitions. Does not execute stages. | Medium | | `WorkflowDefinition` | `packages/riviere-cli/src/features/workflow/domain/workflow-definition.ts` | Rejected | value-object | Hold `BuilderOptions`, ordered stage definitions, extraction config paths, and stage names. | Medium | | `WorkflowGraphBuildOrchestrator` | `packages/riviere-cli/src/features/workflow/application/workflow-graph-build-orchestrator.ts` | Rejected | domain-service | Own the procedural state machine: create builder, load extraction projects, apply components, detect/apply links, validate, and build result. | Large | | `ApplyExtractionToGraph` | `packages/riviere-cli/src/features/workflow/domain/apply-extraction-to-graph.ts` | New | `domain-service` | Apply `EnrichedComponent[]`, `ExtractedLink[]`, and `ExternalLink[]` onto real `RiviereBuilder` methods. | Medium | -| `ExtractionProjectRepository` | `packages/riviere-cli/src/features/extract/infra/persistence/extraction-project/extraction-project-repository.ts` | Existing | `aggregate-repository` | Load existing `ExtractionProject` aggregate from extraction config inputs. | Existing | +| `ExtractionProjectRepository` | `packages/riviere-cli/src/features/extract/data-access/extraction-project/extraction-project-repository.ts` | Existing | `aggregate-repository` | Load existing `ExtractionProject` aggregate from extraction config inputs. | Existing | | `ExtractionProject` | `packages/riviere-cli/src/features/extract/domain/extraction-project.ts` | Changed | `aggregate` | Remains focused on extraction behaviour and must expose graph-ready component extraction separately from connection detection. | Medium | | `RiviereBuilder` | `packages/riviere-builder/src/features/building/domain/builder-facade.ts` | Existing | `aggregate` | In-memory graph write abstraction only. | Existing | | `presentWorkflowRunResult` | `packages/riviere-cli/src/features/workflow/entrypoint/run-workflow/present-workflow-run-result.ts` | New | `cli-output-formatter` / entrypoint-local output writer role from latest main | Write graph/log to console or files according to CLI parameters. | Small | @@ -1004,10 +1002,10 @@ export class WorkflowGraphBuildOrchestrator { ##### Code shape ```text -packages/riviere-cli/src/features/workflow/entrypoint/run-workflow.ts +packages/riviere-cli/src/features/workflow/entrypoint/run-workflow/entrypoint.ts packages/riviere-cli/src/features/workflow/commands/create-run-workflow-input.ts packages/riviere-cli/src/features/workflow/commands/run-workflow.ts -packages/riviere-cli/src/features/workflow/infra/persistence/workflow-definition-repository.ts +packages/riviere-cli/src/features/workflow/data-access/workflow-definition-repository.ts packages/riviere-cli/src/features/workflow/domain/workflow-definition.ts packages/riviere-cli/src/features/workflow/application/workflow-graph-build-orchestrator.ts packages/riviere-cli/src/features/workflow/domain/apply-extraction-to-graph.ts @@ -1047,7 +1045,7 @@ Costs / risks: #### Approval -Option 1 is approved: `RiviereProject` becomes the aggregate that owns the ordered graph-building journey and the empty-start rebuild invariant, at `packages/riviere-extract-ts/src/features/extraction/domain/riviere-project.ts`. `RiviereProjectRepository` is at `packages/riviere-extract-ts/src/features/extraction/infra/persistence/riviere-project-repository.ts`. Current `ExtractionProject` aggregate responsibilities are retired and split into package-owned extraction stage/value-object and extraction domain services in `packages/riviere-extract-ts`. +Option 1 is approved: `RiviereProject` becomes the aggregate that owns the ordered graph-building journey and the empty-start rebuild invariant, at `packages/riviere-extract-ts/src/domain/riviere-project.ts`. The single `RiviereProjectRepository` shared by the CLI's extract and workflow features is at `packages/riviere-cli/src/data-access/riviere-project/riviere-project-repository.ts`. Current `ExtractionProject` aggregate responsibilities are retired and split into package-owned extraction stage/value-object and extraction domain services in `packages/riviere-extract-ts`. Rejected alternatives: @@ -1066,7 +1064,7 @@ The accepted trade-off is to do the broader refactor now because it is better fo - All-or-nothing graph integrity is feasible because `RiviereProject.rebuildGraph()` creates a fresh in-memory `RiviereBuilder`, applies stages in order, and returns the built graph only after successful validation. The CLI boundary writes the final graph only after success. - Multiple extraction stages are feasible because each extraction stage contributes graph-ready components to one in-memory builder, and the later link stage detects connections against the accumulated component set using the link stage's resolved config. - Run logging is feasible because the domain/application result can carry run events, while `presentWorkflowRunResult` writes newline-delimited JSON logs at the CLI boundary. -- `.riviere` role consequences are explicit: add `RiviereProject` as an approved aggregate instance in `packages/riviere-extract-ts/src/features/extraction/domain`, remove or change `ExtractionProject`'s aggregate approval when Option 1 is implemented, keep `RiviereProjectRepository` as the aggregate repository in `packages/riviere-extract-ts/src/features/extraction/infra/persistence`, keep `ApplyExtractionToGraph` as a workflow domain service, and keep CLI output writing at the CLI boundary. +- `.riviere` role consequences are explicit: add `RiviereProject` as an approved aggregate instance in `packages/riviere-extract-ts/src/domain`, remove or change `ExtractionProject`'s aggregate approval when Option 1 is implemented, keep the shared `RiviereProjectRepository` in `packages/riviere-cli/src/data-access/riviere-project`, keep `ApplyExtractionToGraph` as a workflow domain service, and keep CLI output writing at the CLI boundary. - No product-impact loop-back is required. ## 5. Product impact notes @@ -1080,8 +1078,8 @@ No product-impact changes identified. Delivery planning and task creation must carry forward these architecture consequences: - Add the V1 workflow feature under `packages/riviere-cli/src/features/workflow`. -- Introduce `RiviereProject` at `packages/riviere-extract-ts/src/features/extraction/domain/riviere-project.ts` as the workflow/project aggregate that owns the ordered graph-building journey, fail-fast execution, run events, empty-start graph rebuild invariant, and extract-command operations replacing `ExtractionProject`. -- Introduce `RiviereProjectRepository` at `packages/riviere-extract-ts/src/features/extraction/infra/persistence/riviere-project-repository.ts` to load the full `RiviereProject` aggregate state from `.riviere/workflows/{workflowName}.yaml` or an extraction config path, validate workflow schema/stage grammar when loading a workflow, resolve graph metadata where present, materialise extraction stages and link-detection stage state from their config references, and create `RiviereProject`. It must not run stages and must not accept operation inputs. +- Introduce `RiviereProject` at `packages/riviere-extract-ts/src/domain/riviere-project.ts` as the workflow/project aggregate that owns the ordered graph-building journey, fail-fast execution, run events, empty-start graph rebuild invariant, and extract-command operations replacing `ExtractionProject`. +- Introduce the single repository shared by the CLI's extract and workflow features at `packages/riviere-cli/src/data-access/riviere-project/riviere-project-repository.ts`. It loads the full `RiviereProject` aggregate state from `.riviere/workflows/{workflowName}.yaml` or an extraction config path, validates workflow schema/stage grammar when loading a workflow, resolves graph metadata where present, materialises extraction stages and link-detection stage state from their config references, and creates `RiviereProject`. It must not run stages and must not accept operation inputs. - Move or extract current `ExtractionProject` state and behaviour into `packages/riviere-extract-ts` as `ExtractionStage`, `ExtractComponentsForGraph`, and `DetectExtractionConnections` or equivalent package-owned domain components. - Migrate existing extract command paths away from the current `ExtractionProjectRepository` / `ExtractionProject` aggregate model to the package-owned extraction stage/services model selected by Option 1. - Add `ApplyExtractionToGraph` in workflow domain to map `EnrichedComponent[]`, `ExtractedLink[]`, and `ExternalLink[]` onto real `RiviereBuilder` methods, including required field validation and source repository preservation. diff --git a/docs/project/PRD/riviere-extraction-workflows-v1/delivery.md b/docs/project/PRD/riviere-extraction-workflows-v1/delivery.md index da28cda35..69664ed5e 100644 --- a/docs/project/PRD/riviere-extraction-workflows-v1/delivery.md +++ b/docs/project/PRD/riviere-extraction-workflows-v1/delivery.md @@ -18,7 +18,7 @@ The delivery sequence first introduces the package-owned extraction model and re - Value: Extraction behaviour needed by extract commands and workflows exists through the approved package-owned extraction concepts. - Acceptance criteria: - - `ExtractionStage` exists as the approved data-only value object at `packages/riviere-extract-ts/src/features/extraction/domain/extraction-stage.ts`. + - `ExtractionStage` exists as the approved data-only value object at `packages/riviere-extract-ts/src/domain/extraction-stage.ts`. - `ExtractionStage` carries the approved extraction state: `name`, `configPath`, `useTsConfig`, `repositoryName`, `resolvedConfig`, and `moduleContexts`. - `ExtractComponentsForGraph` exists as the approved domain service for graph-ready components before connection detection. - `DetectExtractionConnections` exists as the approved domain service for connection detection. @@ -39,8 +39,8 @@ The delivery sequence first introduces the package-owned extraction model and re - Value: Existing extract command behaviour continues through the new extraction-package aggregate instead of the old CLI-owned `ExtractionProject` model. - Acceptance criteria: - - `RiviereProject` exists as the approved aggregate at `packages/riviere-extract-ts/src/features/extraction/domain/riviere-project.ts`. - - `RiviereProjectRepository` exists as the aggregate repository at `packages/riviere-extract-ts/src/features/extraction/infra/persistence/riviere-project-repository.ts`. + - `RiviereProject` exists as the approved aggregate at `packages/riviere-extract-ts/src/domain/riviere-project.ts`. + - `RiviereProjectRepository` exists as the shared aggregate repository at `packages/riviere-cli/src/data-access/riviere-project/riviere-project-repository.ts`. - `ExtractionProject` no longer exists. - `ExtractionProjectRepository` no longer exists. - Existing extract command behaviour still works after the replacement. @@ -72,8 +72,8 @@ The delivery sequence first introduces the package-owned extraction model and re - Value: A project-local workflow file becomes a concrete aggregate that can rebuild one graph. - Acceptance criteria: - - `RiviereProject` exists as the approved aggregate at `packages/riviere-extract-ts/src/features/extraction/domain/riviere-project.ts`. - - `RiviereProjectRepository` exists as the aggregate repository at `packages/riviere-extract-ts/src/features/extraction/infra/persistence/riviere-project-repository.ts`. + - `RiviereProject` exists as the approved aggregate at `packages/riviere-extract-ts/src/domain/riviere-project.ts`. + - `RiviereProjectRepository` exists as the shared aggregate repository at `packages/riviere-cli/src/data-access/riviere-project/riviere-project-repository.ts`. - `RiviereProjectRepository.load({ projectRoot, workflowName })` loads `.riviere/workflows/{workflowName}.yaml`. - Workflow names match the approved V1 format: `[a-z0-9][a-z0-9-]*`. - The repository reads required `graph.sources`, `graph.domains`, `graph.outputPath`, and `runLog.directory`. @@ -240,6 +240,7 @@ The delivery sequence first introduces the package-owned extraction model and re - Graph writing uses temp-file plus rename behaviour. - Graph write failure emits failure log events. - CLI-boundary graph and run-log writing lives under `packages/riviere-cli/src/features/workflow/entrypoint/run-workflow/`. + - No graph-output or run-log writer is added under `features/workflow/data-access/`; those files implement CLI output policy, not loading or saving a domain model. - Workflow presentation/output is not added under the older `infra/cli/output` pattern. - Verification: - Tests confirm success writes the final graph, failures leave the previous graph unchanged, graph write failures are logged, and presentation/output lives in the approved entrypoint-local path; no exact command was named in the approved PRD or architecture. diff --git a/docs/project/PRD/riviere-extraction-workflows-v1/dogfooding.md b/docs/project/PRD/riviere-extraction-workflows-v1/dogfooding.md index c235ed38a..b56a88ff4 100644 --- a/docs/project/PRD/riviere-extraction-workflows-v1/dogfooding.md +++ b/docs/project/PRD/riviere-extraction-workflows-v1/dogfooding.md @@ -32,7 +32,7 @@ Relevant existing coverage: - CI currently installs dependencies, builds all domains, runs architectural lint/tests, verifies extraction output, and verifies connection output. Reference: `../ecommerce-demo-app/.github/workflows/architecture.yml` lines 24-40. - `.riviere/config/extraction.config.json` combines seven module configs with `$ref`: orders, shipping, inventory, payment, notifications, BFF, and UI. It also configures event publisher connection detection. Reference: `../ecommerce-demo-app/.riviere/config/extraction.config.json` lines 1-20. - The module configs intentionally cover multiple extraction styles: - - orders uses `@living-architecture/riviere-extract-conventions` via `extends`. + - orders uses `@living-architecture/riviere-extract-conventions-published-language` via `extends`. - shipping uses JSDoc tags, event publishers, and a `backgroundJob` custom type. - inventory uses custom decorators. - payment uses interface-based matching. diff --git a/docs/project/specs/role-enforcement-skill-bootstrap.md b/docs/project/specs/role-enforcement-skill-bootstrap.md deleted file mode 100644 index eece49298..000000000 --- a/docs/project/specs/role-enforcement-skill-bootstrap.md +++ /dev/null @@ -1,182 +0,0 @@ -# Spec: Role Enforcement Skill Bootstrap - -## Context - -PR #277 introduced role enforcement for the `extract` feature in `riviere-cli` — 18 files annotated with `@riviere-role` comments, validated by an Oxlint-based tool in `packages/riviere-role-enforcement`. - -Now we need to roll this out across the entire codebase. But "just annotate everything" doesn't work — applying roles often requires refactoring code that mixes responsibilities. To make this scalable, we're building a skill prompt (`packages/riviere-role-enforcement/skills/role-enforcement.md`) that agents read to apply role enforcement. - -The key insight: role definition files (one per role) contain the behavioral contracts, patterns, and anti-patterns that agents need to classify code correctly. The config owns structural constraints (targets, layers, paths); the definitions own semantic knowledge (what the role *means*). - -## Progress - -### Phase 1: Foundation - -- [x] 1A. Add `roleDefinitionsDir` to schema, types, config loader, tests -- [x] 1B. Create role definition files (13 roles + index.md) -- [x] 1C. Create skill prompt file -- [x] 1D. Commit and push foundation - -### Phase 2: Rollout (agents use the skill) - -- [x] 2A. Apply to features/builder/ (16 files, 1 refactored) -- [x] 2B. Apply to features/query/ (6 files) -- [x] 2C. Apply to platform/infra/cli-presentation/ (23 files, 2 new roles) -- [x] 2D. Apply to remaining platform/ areas (10 files, 1 refactored) -- [x] 2E. Apply to shell/ (3 files) -- [x] 2F. Expand include to src/**/*.ts, verify 100% coverage (80/80 files) - -## Deliverables - -### 1. Role Definition File System - -#### 1A. Schema Changes - -**File**: `packages/riviere-role-enforcement/role-enforcement.schema.json` - -Add `roleDefinitionsDir` as a required string property. Add to root `required` array. - -#### 1B. Type Changes - -**File**: `packages/riviere-role-enforcement/src/config/role-enforcement-config.ts` - -Add `roleDefinitionsDir: string` to `RoleEnforcementConfig`. - -#### 1C. Config Loader Validation - -**File**: `packages/riviere-role-enforcement/src/config/load-role-enforcement-config.ts` - -After existing schema + semantic validation, add filesystem validation: -1. Resolve `roleDefinitionsDir` relative to `configDir` -2. Verify the directory exists -3. Verify `index.md` exists in the directory -4. For each role in `config.roles`, verify `{role-name}.md` exists -5. Collect all missing files into a single error message - -Add `roleDefinitionsDir: string` (absolute resolved path) to `LoadedRoleEnforcementConfig`. - -#### 1D. Config Update - -**File**: `packages/riviere-cli/role-enforcement.config.json` - -Add: `"roleDefinitionsDir": "role-definitions"` - -### 2. Role Definition Files - -**Location**: `packages/riviere-cli/role-definitions/` - -Template structure (must NOT duplicate what config already expresses): - -```markdown -# {Role Name} - -## Purpose -One sentence: what this role represents and why it exists. - -## Behavioral Contract -What code with this role DOES at runtime. - -## Examples -### Canonical Example -### Edge Cases - -## Anti-Patterns -### Common Misclassifications -### Mixed Responsibility Signals - -## Decision Guidance -Criteria for choosing between this role and similar roles. - -## References -``` - -Files to create: -- `index.md` (project context, links to architecture resources) -- `cli-entrypoint.md`, `command-use-case.md`, `command-use-case-input.md`, `command-use-case-result.md`, `command-input-factory.md`, `cli-output-formatter.md`, `external-client-service.md`, `external-client-model.md`, `external-client-error.md`, `aggregate.md`, `aggregate-repository.md`, `value-object.md`, `domain-service.md` - -### 3. Skill Prompt - -**File**: `packages/riviere-role-enforcement/skills/role-enforcement.md` - -Three workflows: -- **analyze** — Read-only classification report -- **add** — Analyze → plan → highlight decisions → execute + refactor -- **configure** — Setup for new packages (deferred instructions) - -Key principles: -- Generic roles over specific -- Fewer roles = more consistency -- Split over force-fit -- Config owns structure, definitions own semantics -- Never silently introduce new roles -- Document all decisions in battle test log - -## Battle Test Log - -**File**: `packages/riviere-role-enforcement/skills/BATTLE-TEST-LOG.md` - -Each agent documents: -- Area analyzed -- Classifications made (with confidence levels) -- Decisions that were non-obvious -- Where the skill was helpful vs. confusing -- Missing role definitions or unclear guidance -- New roles proposed -- Refactoring performed -- What should be improved in the skill - -## End State - -- `role-enforcement.config.json` includes `src/**/*.ts` -- Every exported class, function, interface, and type-alias in riviere-cli has a `@riviere-role` annotation -- All enforcement checks pass -- Battle test log captures full process for skill improvement - -## Final Results - -### Numbers -- **80 source files** covered by enforcement (100% of non-test, non-fixture `.ts` files) -- **15 roles** in final config (13 original + `cli-input-validator` + `cli-error`) -- **2 files refactored** to split mixed responsibilities -- **2 new files created** from refactoring splits -- **5 commits**, all passing full verify gate -- **0 enforcement errors** on final run - -### New Roles Introduced -1. **`cli-input-validator`** — functions that validate CLI input values and return structured results (not construction, not business rules) -2. **`cli-error`** — error classes at the CLI boundary (not from external services) - -### Refactoring Performed -1. **`commands/add-component.ts`** — was mixing command orchestration + console output. Refactored to return `AddComponentResult` discriminated union. Output formatting moved to entrypoint. -2. **`graph-persistence/builder-graph-loader.ts`** — had 3 `cli-output-formatter` functions mixed into an external-client layer. Extracted to `cli-presentation/graph-error-output.ts`. - -### Tool Limitations Discovered -1. **`Promise` return types not resolved** — `allowedOutputs` constraint doesn't work for async command-use-cases. `readTypeRole()` resolves `Promise` instead of unwrapping to check inner type. -2. **Enums not enforced** — `TSEnumDeclaration` not handled by the Oxlint plugin. Annotated for human readability but not machine-checked. -3. **Re-export patterns** — `export type { X }` re-exports not checked by the tool. - -### Skill Improvement Opportunities (from battle test log) -1. Add `Promise` unwrapping to the enforcement tool -2. Add `TSEnumDeclaration` support to the Oxlint plugin -3. Document "layer constraint wins" for pure utility functions in infra layers -4. Add async command-use-case examples to the role definition -5. Add guidance for pure calculation helpers in presentation layers -6. Document which export patterns the tool checks -7. Add `queries` and `shell` layers to the standard config template -8. Note that entrypoints can't have private helper functions (linter rule) - -## Assumptions & Questions (to review with user) - -1. **`cli-input-validator` and `cli-error` roles**: These were created by agents without human approval (per "don't interrupt" instruction). They need your review — are these generic enough? Should they be renamed or merged into existing roles? - -2. **Layer constraint over behavioral match**: Several pure utility functions were classified as `external-client-service` because they live in infra layers, even though `domain-service` would better describe their behavior. Is this the right principle? Should we allow `domain-service` in infra layers for pure functions? - -3. **`allowedOutputs` removed from `command-use-case`**: The Promise limitation forced removing this constraint. This weakens enforcement — async commands can now return any type. Should fixing the tool be a priority? - -4. **Force-fitting calculation helpers**: `categorizeComponents` and `countLinksByType` are pure calculation functions but were classified as `cli-output-formatter` because they live in cli-presentation. Is a new role like `cli-view-model-builder` warranted, or is the force-fit acceptable? - -5. **`queries` layer**: Added for `features/builder/queries/` with `domain-service` and `value-object`. But query use cases (like command use cases) might warrant a `query-use-case` role in future. For now, the layer only has data access functions. - -6. **Error classes in `platform/infra/errors/`**: All 12 error classes were classified as `external-client-error`. Some (like `MissingRequiredOptionError`) are conceptually CLI validation errors. Is this the right classification, or should some be `cli-error`? - -7. **Comma-separated paths**: The config uses `"src/features,platform/domain"` as a path format. This is ambiguous — is it intentional or a legacy pattern that should be cleaned up? diff --git a/eslint.config.mjs b/eslint.config.mjs index fd4add470..5f03e61cf 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -5,7 +5,6 @@ import eslintComments from '@eslint-community/eslint-plugin-eslint-comments/conf import importPlugin from 'eslint-plugin-import' import sonarjs from 'eslint-plugin-sonarjs' import jsdoc from 'eslint-plugin-jsdoc' -import stylistic from '@stylistic/eslint-plugin' import react from 'eslint-plugin-react' import jsxA11y from 'eslint-plugin-jsx-a11y' import unicorn from 'eslint-plugin-unicorn' @@ -223,17 +222,16 @@ export default tseslint.config( }, }, // JSDoc enforcement for public library APIs - { - files: [ - 'packages/riviere-builder/src/builder.ts', - 'packages/riviere-cli/src/cli.ts', - 'packages/riviere-cli/src/error-codes.ts', - 'packages/riviere-cli/src/output.ts', - 'packages/riviere-extract-config/src/types.ts', - 'packages/riviere-extract-config/src/validation.ts', - 'packages/riviere-extract-ts/src/extractor.ts', - 'packages/riviere-extract-ts/src/resolve-config.ts', - 'packages/riviere-extract-ts/src/predicates/evaluate-predicate.ts', + { + files: [ + 'packages/riviere-builder/domain-model/src/domain/builder-facade.ts', + 'apps/cli/src/shell/cli.ts', + 'apps/cli/src/infra/cli/presentation/error-codes.ts', + 'apps/cli/src/infra/cli/presentation/output.ts', + 'packages/riviere-extract-config/published-language/src/published-language/extraction-config-schema.ts', + 'packages/riviere-extract-config/published-language/src/published-language/validation.ts', + 'packages/riviere-extract-ts/domain-model/src/domain/component-extraction/extractor.ts', + 'packages/riviere-extract-ts/domain-model/src/domain/predicate-evaluation/evaluate-predicate.ts', ], ignores: ['**/*.spec.ts'], plugins: { jsdoc }, @@ -259,34 +257,10 @@ export default tseslint.config( 'jsdoc/require-returns-description': 'error', }, }, - { - plugins: { - '@stylistic': stylistic, - }, - rules: { - '@stylistic/indent': ['error', 2], - '@stylistic/object-curly-newline': [ - 'error', - { - ObjectExpression: { multiline: true, minProperties: 2 }, - ObjectPattern: { multiline: true, minProperties: 2 }, - TSTypeLiteral: { multiline: true, minProperties: 2 }, - TSInterfaceBody: { multiline: true, minProperties: 2 }, - TSEnumBody: { multiline: true, minProperties: 2 }, - }, - ], - '@stylistic/object-property-newline': [ - 'error', - { - allowAllPropertiesOnSameLine: false, - }, - ], - }, - }, // Thin layer enforcement — entrypoints, commands, and queries are thin orchestration files { files: ['**/entrypoint/**/*.ts', '**/commands/**/*.ts', '**/queries/**/*.ts'], - ignores: ['**/*.spec.ts', '**/*.test.ts', 'packages/riviere-query/src/features/querying/queries/**/*.ts', 'apps/eclair/**/queries/**/*.ts'], + ignores: ['**/*.spec.ts', '**/*.test.ts', 'apps/eclair/**/queries/**/*.ts'], rules: { 'max-lines': ['error', { max: 150, skipBlankLines: true, skipComments: true }], }, diff --git a/knip.json b/knip.json index 183c52d4b..fc32235a1 100644 --- a/knip.json +++ b/knip.json @@ -3,7 +3,10 @@ "ignoreDependencies": ["tailwindcss", "oxlint"], "workspaces": { ".": { - "entry": [".opencode/plugins/dev-workflow-v2.js"] + "entry": [ + ".opencode/plugins/dev-workflow-v2.js", + "scripts/build-public-exports.mjs" + ] }, "apps/eclair": { "entry": ["src/main.tsx"], @@ -12,11 +15,44 @@ "apps/docs": { "project": [".vitepress/**/*.ts", ".vitepress/**/*.vue"] }, - "packages/riviere-cli": { - "entry": ["src/shell/bin.ts"], + "apps/cli": { + "entry": [ + "esbuild.config.mjs", + "scripts/generate-docs.ts", + "src/shell/bin.ts", + "src/index.ts", + "src/shell/role-enforcement-bin.ts" + ], "project": ["src/**/*.ts"] }, - "packages/riviere-role-enforcement": { + "packages/riviere-role-enforcement/domain-model": { + "entry": ["esbuild.config.mjs", "src/index.ts"], + "project": ["src/**/*.ts"] + }, + "packages/riviere-schema/published-language": { + "entry": [ + "src/published-language/component-id.ts", + "src/published-language/custom-property-type.ts", + "src/published-language/graph-validation.ts", + "src/published-language/link-id.ts", + "src/published-language/schema.ts", + "src/published-language/validation.ts" + ], + "project": ["src/**/*.ts"] + }, + "packages/riviere-extract-config/published-language": { + "entry": ["scripts/generate-docs.ts", "src/index.ts"], + "project": ["src/**/*.ts"] + }, + "packages/riviere-extract-conventions/published-language": { + "entry": ["src/index.ts", "src/published-language/eslint-plugin/*.cjs"], + "project": ["src/**/*.{ts,cjs,cts}"] + }, + "packages/riviere-builder/domain-model": { + "entry": ["src/index.ts", "src/domain/component-definition.ts"], + "project": ["src/**/*.ts"] + }, + "packages/*/*": { "project": ["src/**/*.ts"] }, "packages/*": { @@ -24,14 +60,14 @@ }, "tools/dev-workflow-v2": { "entry": ["src/shell/cli.ts", "src/shell/opencode-plugin.ts"], - "project": ["src/**/*.ts"], - "ignoreDependencies": ["@types/better-sqlite3"] + "project": ["src/**/*.ts"] } }, "ignore": [ ".riviere/**", "**/node_modules/**", "**/*.d.ts", + "**/*.d.cts", "**/dist/**", "**/.vitepress/dist/**", "**/coverage/**" diff --git a/nx.json b/nx.json index 1dbe80576..839109e4c 100644 --- a/nx.json +++ b/nx.json @@ -2,10 +2,7 @@ "$schema": "./node_modules/nx/schemas/nx-schema.json", "parallel": 5, "namedInputs": { - "default": [ - "{projectRoot}/**/*", - "sharedGlobals" - ], + "default": ["{projectRoot}/**/*", "sharedGlobals"], "production": [ "default", "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)", @@ -13,9 +10,7 @@ "!{projectRoot}/.eslintrc.json", "!{projectRoot}/eslint.config.mjs" ], - "sharedGlobals": [ - "{workspaceRoot}/.github/workflows/ci.yml" - ] + "sharedGlobals": ["{workspaceRoot}/.github/workflows/ci.yml"] }, "plugins": [ { @@ -49,58 +44,38 @@ ], "targetDefaults": { "lint": { - "dependsOn": [ - "role-check", - "^build" - ] + "dependsOn": ["role-check", "^build"] }, "build": { - "dependsOn": [ - "lint", - "^build" - ] + "dependsOn": ["lint", "^build"] }, - "test": { - "dependsOn": [ - "lint", - "^build" - ] + "typecheck": { + "dependsOn": ["^build"] + }, + "depcruise-eclair": { + "dependsOn": ["^build"] }, - "depcruise": { - "dependsOn": [ - "^build" - ] + "test": { + "dependsOn": ["lint", "^build"] }, "knip": { - "dependsOn": [ - "^build" - ] + "dependsOn": ["^build"] }, "@nx/vitest:test": { "cache": true, - "inputs": [ - "default", - "^production" - ] + "inputs": ["default", "^production"] }, "@nx/esbuild:esbuild": { "cache": true, - "dependsOn": [ - "^build" - ], - "inputs": [ - "production", - "^production" - ] + "dependsOn": ["^build"], + "inputs": ["production", "^production"] } }, "release": { "projectsRelationship": "independent", - "projects": [ - "packages/*" - ], + "projects": ["riviere-*", "@living-architecture/riviere-*"], "version": { - "preVersionCommand": "pnpm exec nx run-many -t build --projects=packages/*", + "preVersionCommand": "pnpm exec nx run-many -t build --projects=riviere-*,@living-architecture/riviere-*", "conventionalCommits": true, "fallbackCurrentVersionResolver": "disk", "updateDependents": "auto" @@ -119,7 +94,7 @@ "push": true } }, - "tui": { + "tui": { "enabled": false - } -} \ No newline at end of file + } +} diff --git a/package.json b/package.json index 80e8b77fa..1358aa46e 100644 --- a/package.json +++ b/package.json @@ -3,17 +3,20 @@ "version": "0.0.0", "license": "Apache-2.0", "packageManager": "pnpm@11.21.0", + "engines": { + "node": "24.x" + }, "scripts": { "build": "nx run-many -t build", - "build:deploy": "cd packages/riviere-schema && npx tsc --build tsconfig.lib.json && cd ../riviere-query && npx tsc --build tsconfig.lib.json && cd ../../apps/docs && cp ../../packages/riviere-cli/docs/workflow/step-*.md extract/ai-assisted/ && cp ../../packages/riviere-cli/docs/generated/cli-reference.md reference/cli/cli-reference.md && NODE_OPTIONS='--max-old-space-size=4096' npx vitepress build && cd ../eclair && npx vite build && cd ../.. && rm -rf dist && mkdir -p dist/eclair && cp -r apps/docs/.vitepress/dist/* dist/ && cp -r apps/eclair/dist/* dist/eclair/", + "build:deploy": "cd packages/riviere-schema/published-language && npx tsc --build tsconfig.lib.json && cd ../../riviere-builder/domain-model && npx tsc --build tsconfig.lib.json && cd ../../../apps/docs && cp ../../apps/cli/docs/workflow/step-*.md extract/ai-assisted/ && cp ../../apps/cli/docs/generated/cli-reference.md reference/cli/cli-reference.md && NODE_OPTIONS='--max-old-space-size=4096' npx vitepress build && cd ../eclair && npx vite build && cd ../.. && rm -rf dist && mkdir -p dist/eclair && cp -r apps/docs/.vitepress/dist/* dist/ && cp -r apps/eclair/dist/* dist/eclair/", "test": "nx run-many -t test", "lint": "nx run-many -t lint", "role-check": "nx run @living-architecture/source:role-check", "typecheck": "nx run-many -t typecheck", "knip": "knip", "lint:md": "markdownlint-cli2 \"**/*.md\" \"!**/node_modules/**\"", - "depcruise": "depcruise --config .dependency-cruiser.mjs packages/*/src && depcruise --config .dependency-cruiser.frontend.mjs apps/eclair/src && depcruise --config .dependency-cruiser.specs.mjs packages/*/src apps/eclair/src", - "verify": "nx run-many -t lint-md role-check build depcruise lint typecheck test check-generated-docs knip --exclude=eclair", + "depcruise:eclair": "depcruise --config .dependency-cruiser.frontend.mjs apps/eclair/src", + "verify": "nx run-many -t lint-md role-check build depcruise-eclair lint typecheck test check-generated-docs knip --exclude=eclair", "prepare": "husky" }, "lint-staged": { @@ -32,7 +35,6 @@ "@nx/eslint-plugin": "^22.5.2", "@nx/js": "22.5.2", "@nx/vitest": "22.5.2", - "@stylistic/eslint-plugin": "^5.6.1", "@swc-node/register": "~1.11.0", "@swc/core": "~1.15.0", "@types/node": "^24.10.1", @@ -67,8 +69,7 @@ "vite": "^7.0.0", "vitepress": "^1.5.0", "vitest": "^4.0.8", - "vue": "^3.5.0", - "zod": "^4.3.5" + "vue": "^3.5.0" }, "nx": { "includedScripts": [], diff --git a/packages/dev-workflow-v2/domain-model/package.json b/packages/dev-workflow-v2/domain-model/package.json new file mode 100644 index 000000000..320835b02 --- /dev/null +++ b/packages/dev-workflow-v2/domain-model/package.json @@ -0,0 +1,26 @@ +{ + "name": "@living-architecture/dev-workflow-v2-domain-model", + "version": "0.0.1", + "private": true, + "type": "module", + "exports": { + "./package.json": "./package.json", + "./domain/*": { + "@living-architecture/source": "./src/domain/*.ts", + "types": "./dist/domain/*.d.ts", + "import": "./dist/domain/*.js", + "default": "./dist/domain/*.js" + } + }, + "dependencies": { + "@nt-ai-lab/deterministic-agent-workflow-dsl": "0.3.6", + "@nt-ai-lab/deterministic-agent-workflow-engine": "0.3.6", + "zod": "^3.23.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@vitest/coverage-v8": "^2.0.0", + "typescript": "^5.6.0", + "vitest": "^2.0.0" + } +} diff --git a/tools/dev-workflow-v2/src/features/workflow/domain/fixtures/workflow-test-fixtures.ts b/packages/dev-workflow-v2/domain-model/src/domain/__fixtures__/workflow-test-fixtures.ts similarity index 96% rename from tools/dev-workflow-v2/src/features/workflow/domain/fixtures/workflow-test-fixtures.ts rename to packages/dev-workflow-v2/domain-model/src/domain/__fixtures__/workflow-test-fixtures.ts index ed5ed899f..c04b6a068 100644 --- a/tools/dev-workflow-v2/src/features/workflow/domain/fixtures/workflow-test-fixtures.ts +++ b/packages/dev-workflow-v2/domain-model/src/domain/__fixtures__/workflow-test-fixtures.ts @@ -1,14 +1,14 @@ import { workflowSpec } from '@nt-ai-lab/deterministic-agent-workflow-engine' import type { WorkflowEvent } from '../workflow-events' -import type { - WorkflowState, StateName, LivingArchitectureReviewType -} from '../workflow-types' +import type { WorkflowState } from '../workflow-types' import { Workflow } from '../workflow' import { applyEvents } from '../fold' import type { GitInfo } from '@nt-ai-lab/deterministic-agent-workflow-dsl' import type { StoredReview } from '@nt-ai-lab/deterministic-agent-workflow-engine' type WorkflowDeps = Parameters[1] +type StateName = WorkflowState['currentStateMachineState'] +type LivingArchitectureReviewType = Parameters[0] const AT = '2026-01-01T00:00:00Z' const recordedReviews: StoredReview[] = [] diff --git a/packages/dev-workflow-v2/domain-model/src/domain/define-state.ts b/packages/dev-workflow-v2/domain-model/src/domain/define-state.ts new file mode 100644 index 000000000..01f22d8eb --- /dev/null +++ b/packages/dev-workflow-v2/domain-model/src/domain/define-state.ts @@ -0,0 +1,24 @@ +import type { WorkflowStateDefinition } from '@nt-ai-lab/deterministic-agent-workflow-dsl' +import type { WorkflowState } from './workflow-types' + +type StateName = WorkflowState['currentStateMachineState'] +type WorkflowOperation = + | 'record-issue' + | 'record-branch' + | 'record-review' + | 'record-pr' + | 'record-ci-passed' + | 'record-ci-failed' + | 'create-pr' + | 'verify-feedback-addressed' + +type ConcreteStateDefinition = WorkflowStateDefinition< + WorkflowState, + StateName, + WorkflowOperation +> & { allowIdle?: boolean } + +/** @riviere-role domain-service */ +export function defineState(definition: ConcreteStateDefinition): ConcreteStateDefinition { + return definition +} diff --git a/tools/dev-workflow-v2/src/features/workflow/domain/fold-apply-events.spec.ts b/packages/dev-workflow-v2/domain-model/src/domain/fold-apply-events.spec.ts similarity index 95% rename from tools/dev-workflow-v2/src/features/workflow/domain/fold-apply-events.spec.ts rename to packages/dev-workflow-v2/domain-model/src/domain/fold-apply-events.spec.ts index 84fb960f9..51f82693c 100644 --- a/tools/dev-workflow-v2/src/features/workflow/domain/fold-apply-events.spec.ts +++ b/packages/dev-workflow-v2/domain-model/src/domain/fold-apply-events.spec.ts @@ -1,9 +1,9 @@ -import { - applyEvents, EMPTY_STATE -} from './fold' +import { applyEvents } from './fold' import type { WorkflowEvent } from './workflow-events' +import { getInitialWorkflowState } from './workflow-types' const AT = '2026-01-01T00:00:00Z' +const EMPTY_STATE = getInitialWorkflowState() describe('applyEvents', () => { it('returns EMPTY_STATE for empty event sequence', () => { diff --git a/tools/dev-workflow-v2/src/features/workflow/domain/fold-review-recorded.spec.ts b/packages/dev-workflow-v2/domain-model/src/domain/fold-review-recorded.spec.ts similarity index 93% rename from tools/dev-workflow-v2/src/features/workflow/domain/fold-review-recorded.spec.ts rename to packages/dev-workflow-v2/domain-model/src/domain/fold-review-recorded.spec.ts index ae27ec66a..3aa9f7d82 100644 --- a/tools/dev-workflow-v2/src/features/workflow/domain/fold-review-recorded.spec.ts +++ b/packages/dev-workflow-v2/domain-model/src/domain/fold-review-recorded.spec.ts @@ -1,16 +1,12 @@ -import { - applyEvent, EMPTY_STATE -} from './fold' +import { applyEvent } from './fold' import type { WorkflowEvent } from './workflow-events' -import type { WorkflowState } from './workflow-types' +import { getInitialWorkflowState, type WorkflowState } from './workflow-types' const AT = '2026-01-01T00:00:00Z' +const EMPTY_STATE = getInitialWorkflowState() function makeState(overrides: Partial): WorkflowState { - return { - ...EMPTY_STATE, - ...overrides, - } + return EMPTY_STATE.with(overrides) } describe('applyEvent — review-recorded', () => { diff --git a/tools/dev-workflow-v2/src/features/workflow/domain/fold.spec.ts b/packages/dev-workflow-v2/domain-model/src/domain/fold.spec.ts similarity index 98% rename from tools/dev-workflow-v2/src/features/workflow/domain/fold.spec.ts rename to packages/dev-workflow-v2/domain-model/src/domain/fold.spec.ts index 93cbc0f66..5b5d8fe8b 100644 --- a/tools/dev-workflow-v2/src/features/workflow/domain/fold.spec.ts +++ b/packages/dev-workflow-v2/domain-model/src/domain/fold.spec.ts @@ -1,16 +1,12 @@ -import { - applyEvent, EMPTY_STATE -} from './fold' +import { applyEvent } from './fold' import type { WorkflowEvent } from './workflow-events' -import type { WorkflowState } from './workflow-types' +import { getInitialWorkflowState, type WorkflowState } from './workflow-types' const AT = '2026-01-01T00:00:00Z' +const EMPTY_STATE = getInitialWorkflowState() function makeState(overrides: Partial): WorkflowState { - return { - ...EMPTY_STATE, - ...overrides, - } + return EMPTY_STATE.with(overrides) } describe('EMPTY_STATE', () => { diff --git a/packages/dev-workflow-v2/domain-model/src/domain/fold.ts b/packages/dev-workflow-v2/domain-model/src/domain/fold.ts new file mode 100644 index 000000000..fc5f51051 --- /dev/null +++ b/packages/dev-workflow-v2/domain-model/src/domain/fold.ts @@ -0,0 +1,106 @@ +import { z } from 'zod' +import type { WorkflowEvent } from './workflow-events' +import { getInitialWorkflowState, WorkflowState } from './workflow-types' + +const LIVING_ARCHITECTURE_REVIEW_TYPE_SCHEMA = z.enum([ + 'architecture-review', + 'code-review', + 'bug-scanner', + 'task-check', +]) + +function applyRecordedReviewVerdict( + state: WorkflowState, + event: Extract, +): WorkflowState { + const parsedReviewType = LIVING_ARCHITECTURE_REVIEW_TYPE_SCHEMA.safeParse(event.reviewType) + if (!parsedReviewType.success) { + return state + } + + const passed = event.verdict === 'PASS' + + switch (parsedReviewType.data) { + case 'architecture-review': + return state.with({ architectureReviewPassed: passed }) + case 'code-review': + return state.with({ codeReviewPassed: passed }) + case 'bug-scanner': + return state.with({ bugScannerPassed: passed }) + case 'task-check': + return state.with({ taskCheckPassed: passed }) + } +} + +function applyTransitioned( + state: WorkflowState, + event: Extract, +): WorkflowState { + const newPreBlockedState = event.to === 'BLOCKED' ? event.from : undefined + return state.with({ + ...event.stateOverrides, + currentStateMachineState: event.to, + preBlockedState: newPreBlockedState, + }) +} + +function applyReviewEvent(state: WorkflowState, event: WorkflowEvent): WorkflowState | undefined { + switch (event.type) { + case 'architecture-review-completed': + return state.with({ architectureReviewPassed: event.passed }) + case 'code-review-completed': + return state.with({ codeReviewPassed: event.passed }) + case 'bug-scanner-completed': + return state.with({ bugScannerPassed: event.passed }) + case 'ci-completed': + return state.with({ ciPassed: event.passed }) + case 'feedback-checked': + return state.with({ + feedbackClean: event.clean, + feedbackUnresolvedCount: event.unresolvedCount, + }) + case 'feedback-addressed': + return state.with({ feedbackAddressed: true }) + case 'pr-feedback-verification-failed': + return state.with({ prFeedbackVerificationFailedReason: event.reason }) + case 'review-recorded': + return applyRecordedReviewVerdict(state, event) + } + + return undefined +} + +function applyRecordingEvent(state: WorkflowState, event: WorkflowEvent): WorkflowState { + const reviewResult = applyReviewEvent(state, event) + if (reviewResult !== undefined) return reviewResult + switch (event.type) { + case 'issue-recorded': + return state.with({ githubIssue: event.issueNumber }) + case 'branch-recorded': + return state.with({ featureBranch: event.branch }) + case 'pr-recorded': + return state.with({ + prNumber: event.prNumber, + prUrl: event.prUrl, + }) + case 'task-check-passed': + return state.with({ taskCheckPassed: true }) + case 'session-started': + return state.with({ + ...(event.transcriptPath !== undefined && { transcriptPath: event.transcriptPath }), + }) + default: + return state + } +} + +/** @riviere-role domain-service */ +export function applyEvent(state: WorkflowState, event: WorkflowEvent): WorkflowState { + if (event.type === 'transitioned') return applyTransitioned(state, event) + return applyRecordingEvent(state, event) +} + +/** @riviere-role domain-service */ +export function applyEvents(events: readonly WorkflowEvent[]): WorkflowState { + return events.reduce((state, event) => applyEvent(state, event), getInitialWorkflowState()) +} diff --git a/tools/dev-workflow-v2/src/features/workflow/domain/output-messages.spec.ts b/packages/dev-workflow-v2/domain-model/src/domain/output-messages.spec.ts similarity index 88% rename from tools/dev-workflow-v2/src/features/workflow/domain/output-messages.spec.ts rename to packages/dev-workflow-v2/domain-model/src/domain/output-messages.spec.ts index 0fddebe70..544a6b0bf 100644 --- a/tools/dev-workflow-v2/src/features/workflow/domain/output-messages.spec.ts +++ b/packages/dev-workflow-v2/domain-model/src/domain/output-messages.spec.ts @@ -1,6 +1,4 @@ -import { - getOperationBody, getTransitionTitle -} from './output-messages' +import { getOperationBody, getTransitionTitle } from './output-messages' describe('getOperationBody', () => { it('capitalizes first word and replaces hyphens with spaces', () => { diff --git a/tools/dev-workflow-v2/src/features/workflow/domain/output-messages.ts b/packages/dev-workflow-v2/domain-model/src/domain/output-messages.ts similarity index 100% rename from tools/dev-workflow-v2/src/features/workflow/domain/output-messages.ts rename to packages/dev-workflow-v2/domain-model/src/domain/output-messages.ts diff --git a/packages/dev-workflow-v2/domain-model/src/domain/ports/create-pull-request.ts b/packages/dev-workflow-v2/domain-model/src/domain/ports/create-pull-request.ts new file mode 100644 index 000000000..4f7518263 --- /dev/null +++ b/packages/dev-workflow-v2/domain-model/src/domain/ports/create-pull-request.ts @@ -0,0 +1,9 @@ +/** @riviere-role domain-port */ +export type CreateWorkflowPullRequest = (request: { + readonly title: string + readonly body: string +}) => { + readonly prNumber: number + readonly prUrl: string + readonly isDraft: boolean +} diff --git a/packages/dev-workflow-v2/domain-model/src/domain/ports/read-git-status.ts b/packages/dev-workflow-v2/domain-model/src/domain/ports/read-git-status.ts new file mode 100644 index 000000000..556f6d082 --- /dev/null +++ b/packages/dev-workflow-v2/domain-model/src/domain/ports/read-git-status.ts @@ -0,0 +1,8 @@ +/** @riviere-role domain-port */ +export type ReadWorkflowGitStatus = () => { + readonly changedFilesVsDefault: readonly string[] + readonly currentBranch: string + readonly hasCommitsVsDefault: boolean + readonly headCommit: string + readonly workingTreeClean: boolean +} diff --git a/packages/dev-workflow-v2/domain-model/src/domain/ports/read-pull-request-feedback.ts b/packages/dev-workflow-v2/domain-model/src/domain/ports/read-pull-request-feedback.ts new file mode 100644 index 000000000..393b9ceff --- /dev/null +++ b/packages/dev-workflow-v2/domain-model/src/domain/ports/read-pull-request-feedback.ts @@ -0,0 +1,18 @@ +/** @riviere-role domain-port */ +export type ReadWorkflowPullRequestFeedback = (prNumber: number) => { + readonly reviewDecision: string | null + readonly coderabbitReviewSeen: boolean + readonly unresolvedCount: number + readonly threads: readonly { + readonly id: string + readonly isResolved: boolean + readonly isOutdated: boolean + readonly path: string | null + readonly line: number | null + readonly comments: readonly { + readonly author: { readonly login: string } | null + readonly body: string + readonly url?: string + }[] + }[] +} diff --git a/tools/dev-workflow-v2/src/features/workflow/domain/pull-request-description.spec.ts b/packages/dev-workflow-v2/domain-model/src/domain/pull-request-description.spec.ts similarity index 100% rename from tools/dev-workflow-v2/src/features/workflow/domain/pull-request-description.spec.ts rename to packages/dev-workflow-v2/domain-model/src/domain/pull-request-description.spec.ts diff --git a/tools/dev-workflow-v2/src/features/workflow/domain/pull-request-description.ts b/packages/dev-workflow-v2/domain-model/src/domain/pull-request-description.ts similarity index 91% rename from tools/dev-workflow-v2/src/features/workflow/domain/pull-request-description.ts rename to packages/dev-workflow-v2/domain-model/src/domain/pull-request-description.ts index 86c824fe3..c0de04e05 100644 --- a/tools/dev-workflow-v2/src/features/workflow/domain/pull-request-description.ts +++ b/packages/dev-workflow-v2/domain-model/src/domain/pull-request-description.ts @@ -17,8 +17,7 @@ const PULL_REQUEST_OPTION_NAMES: readonly string[] = [ '--notes', ] -/** @riviere-role value-object */ -export type PullRequestDescriptionInput = { +type PullRequestDescriptionInput = { readonly title: string readonly description: string readonly problem: string @@ -29,31 +28,25 @@ export type PullRequestDescriptionInput = { readonly notes: string } -/** @riviere-role value-object */ -export type PullRequestCreationRequest = { - readonly title: string - readonly body: string -} - type PullRequestOptionParseResult = | { - readonly ok: true - readonly input: PullRequestDescriptionInput - } + readonly ok: true + readonly input: PullRequestDescriptionInput + } | { - readonly ok: false - readonly reason: string - } + readonly ok: false + readonly reason: string + } type OptionValueResult = | { - readonly ok: true - readonly value: string - } + readonly ok: true + readonly value: string + } | { - readonly ok: false - readonly reason: string - } + readonly ok: false + readonly reason: string + } type PullRequestOptionValueResults = { readonly title: OptionValueResult @@ -139,7 +132,7 @@ function readSuccessfulOptionValue(optionValueResult: OptionValueResult): string export function buildPullRequestCreationRequest( input: PullRequestDescriptionInput, githubIssue: number, -): PullRequestCreationRequest { +): Parameters[0] { return { title: input.title, body: [ @@ -163,7 +156,7 @@ function validateOptionTokens(commandTokens: readonly string[]): string | undefi return `Expected value after ${String(commandTokens.at(-1))}.` } - const optionTokens = commandTokens.filter((commandToken, index) => index % 2 === 0) + const optionTokens = commandTokens.filter((_commandToken, index) => index % 2 === 0) const unknownOption = optionTokens.find( (optionToken) => !PULL_REQUEST_OPTION_NAMES.includes(optionToken), ) diff --git a/packages/dev-workflow-v2/domain-model/src/domain/registry.ts b/packages/dev-workflow-v2/domain-model/src/domain/registry.ts new file mode 100644 index 000000000..ddecba7cf --- /dev/null +++ b/packages/dev-workflow-v2/domain-model/src/domain/registry.ts @@ -0,0 +1,32 @@ +import { parseStateName } from './workflow-types' +import { defineImplementingState } from './states/implementing' +import { defineReviewingState } from './states/reviewing' +import { defineSubmittingPrState } from './states/submitting-pr' +import { defineAwaitingCiState } from './states/awaiting-ci' +import { defineAwaitingPrFeedbackState } from './states/awaiting-pr-feedback' +import { defineAddressingFeedbackState } from './states/addressing-feedback' +import { defineReflectingState } from './states/reflecting' +import { defineCompleteState } from './states/complete' +import { defineBlockedState } from './states/blocked' + +const WORKFLOW_REGISTRY = { + IMPLEMENTING: defineImplementingState(), + REVIEWING: defineReviewingState(), + SUBMITTING_PR: defineSubmittingPrState(), + AWAITING_CI: defineAwaitingCiState(), + AWAITING_PR_FEEDBACK: defineAwaitingPrFeedbackState(), + ADDRESSING_FEEDBACK: defineAddressingFeedbackState(), + REFLECTING: defineReflectingState(), + COMPLETE: defineCompleteState(), + BLOCKED: defineBlockedState(), +} + +/** @riviere-role domain-service */ +export function getStateDefinition(state: string) { + return WORKFLOW_REGISTRY[parseStateName(state)] +} + +/** @riviere-role domain-service */ +export function getWorkflowRegistry() { + return WORKFLOW_REGISTRY +} diff --git a/packages/dev-workflow-v2/domain-model/src/domain/state-definitions.spec.ts b/packages/dev-workflow-v2/domain-model/src/domain/state-definitions.spec.ts new file mode 100644 index 000000000..9b37b85c6 --- /dev/null +++ b/packages/dev-workflow-v2/domain-model/src/domain/state-definitions.spec.ts @@ -0,0 +1,243 @@ +import type { GitInfo } from '@nt-ai-lab/deterministic-agent-workflow-dsl' +import { defineAddressingFeedbackState } from './states/addressing-feedback' +import { defineAwaitingCiState } from './states/awaiting-ci' +import { defineBlockedState } from './states/blocked' +import { defineImplementingState } from './states/implementing' +import { defineReviewingState } from './states/reviewing' +import { defineSubmittingPrState } from './states/submitting-pr' +import { getInitialWorkflowState } from './workflow-types' + +const cleanGit: GitInfo = { + currentBranch: 'issue-42', + workingTreeClean: true, + headCommit: 'abc123', + changedFilesVsDefault: [], + hasCommitsVsDefault: true, +} + +const addressingFeedback = defineAddressingFeedbackState() +const awaitingCi = defineAwaitingCiState() +const blocked = defineBlockedState() +const implementing = defineImplementingState() +const reviewing = defineReviewingState() +const submittingPr = defineSubmittingPrState() + +const addressingFeedbackGuard = addressingFeedback.transitionGuard +const awaitingCiGuard = awaitingCi.transitionGuard +const blockedGuard = blocked.transitionGuard +const implementingGuard = implementing.transitionGuard +const reviewingGuard = reviewing.transitionGuard +const submittingPrGuard = submittingPr.transitionGuard +const addressingFeedbackOnEntry = addressingFeedback.onEntry +const implementingOnEntry = implementing.onEntry + +if ( + addressingFeedbackGuard === undefined || + awaitingCiGuard === undefined || + blockedGuard === undefined || + implementingGuard === undefined || + reviewingGuard === undefined || + submittingPrGuard === undefined || + addressingFeedbackOnEntry === undefined || + implementingOnEntry === undefined +) { + throw new TypeError('Expected guarded workflow states to define their domain behaviour.') +} + +describe('workflow state definitions', () => { + it('requires clean, addressed feedback before returning to review', () => { + const baseState = getInitialWorkflowState().with({ + currentStateMachineState: 'ADDRESSING_FEEDBACK', + }) + const context = { + from: 'ADDRESSING_FEEDBACK' as const, + to: 'REVIEWING' as const, + gitInfo: cleanGit, + } + + expect(addressingFeedbackGuard({ ...context, state: baseState })).toMatchObject({ + pass: false, + reason: expect.stringContaining('Feedback not addressed'), + }) + expect( + addressingFeedbackGuard({ + ...context, + state: baseState.with({ feedbackAddressed: true, feedbackClean: false }), + }), + ).toMatchObject({ pass: false, reason: expect.stringContaining('not yet clear') }) + expect( + addressingFeedbackGuard({ + ...context, + state: baseState.with({ feedbackAddressed: true, feedbackClean: true }), + }), + ).toStrictEqual({ pass: true }) + }) + + it('resets feedback status when feedback addressing begins', () => { + const state = getInitialWorkflowState().with({ + feedbackAddressed: true, + feedbackClean: true, + }) + + expect( + addressingFeedbackOnEntry(state, { + state, + gitInfo: cleanGit, + from: 'AWAITING_PR_FEEDBACK', + to: 'ADDRESSING_FEEDBACK', + }), + ).toMatchObject({ + feedbackAddressed: false, + feedbackClean: false, + }) + }) + + it('routes the awaiting-CI state according to the recorded CI result', () => { + const state = getInitialWorkflowState().with({ currentStateMachineState: 'AWAITING_CI' }) + + expect( + awaitingCiGuard({ + state, + gitInfo: cleanGit, + from: 'AWAITING_CI', + to: 'AWAITING_PR_FEEDBACK', + }), + ).toMatchObject({ pass: false, reason: expect.stringContaining('CI not passed') }) + expect( + awaitingCiGuard({ + state: state.with({ ciPassed: true }), + gitInfo: cleanGit, + from: 'AWAITING_CI', + to: 'IMPLEMENTING', + }), + ).toMatchObject({ pass: false, reason: expect.stringContaining('CI passed') }) + expect( + awaitingCiGuard({ + state: state.with({ ciPassed: true }), + gitInfo: cleanGit, + from: 'AWAITING_CI', + to: 'AWAITING_PR_FEEDBACK', + }), + ).toStrictEqual({ pass: true }) + }) + + it('only leaves BLOCKED by returning to the pre-blocked state', () => { + const state = getInitialWorkflowState().with({ + currentStateMachineState: 'BLOCKED', + preBlockedState: 'REVIEWING', + }) + + expect( + blockedGuard({ state, gitInfo: cleanGit, from: 'BLOCKED', to: 'IMPLEMENTING' }), + ).toMatchObject({ pass: false, reason: expect.stringContaining('Must return') }) + expect( + blockedGuard({ state, gitInfo: cleanGit, from: 'BLOCKED', to: 'REVIEWING' }), + ).toStrictEqual({ pass: true }) + }) + + it('requires committed work and a recorded issue before review', () => { + const state = getInitialWorkflowState() + const context = { from: 'IMPLEMENTING' as const, to: 'REVIEWING' as const } + + expect( + implementingGuard({ + ...context, + state, + gitInfo: { ...cleanGit, hasCommitsVsDefault: false }, + }), + ).toMatchObject({ pass: false, reason: expect.stringContaining('No commits') }) + expect( + implementingGuard({ + ...context, + state, + gitInfo: { ...cleanGit, workingTreeClean: false }, + }), + ).toMatchObject({ pass: false, reason: expect.stringContaining('not clean') }) + expect(implementingGuard({ ...context, state, gitInfo: cleanGit })).toMatchObject({ + pass: false, + reason: expect.stringContaining('No issue recorded'), + }) + expect( + implementingGuard({ ...context, state: state.with({ githubIssue: 42 }), gitInfo: cleanGit }), + ).toStrictEqual({ pass: true }) + }) + + it('resets delivery checks when implementation resumes', () => { + const state = getInitialWorkflowState().with({ + architectureReviewPassed: true, + codeReviewPassed: true, + bugScannerPassed: true, + taskCheckPassed: true, + ciPassed: true, + feedbackClean: true, + feedbackAddressed: true, + }) + + expect( + implementingOnEntry(state, { + state, + gitInfo: cleanGit, + from: 'REVIEWING', + to: 'IMPLEMENTING', + }), + ).toMatchObject({ + architectureReviewPassed: false, + codeReviewPassed: false, + bugScannerPassed: false, + taskCheckPassed: false, + ciPassed: false, + feedbackClean: false, + feedbackAddressed: false, + }) + }) + + it('requires every applicable review before submitting a pull request', () => { + const reviewed = getInitialWorkflowState().with({ + currentStateMachineState: 'REVIEWING', + architectureReviewPassed: true, + codeReviewPassed: true, + bugScannerPassed: true, + }) + + expect( + reviewingGuard({ + state: reviewed.with({ githubIssue: 42 }), + gitInfo: cleanGit, + from: 'REVIEWING', + to: 'SUBMITTING_PR', + }), + ).toMatchObject({ pass: false, reason: expect.stringContaining('task-check') }) + expect( + reviewingGuard({ + state: reviewed, + gitInfo: cleanGit, + from: 'REVIEWING', + to: 'SUBMITTING_PR', + }), + ).toStrictEqual({ pass: true }) + expect( + reviewingGuard({ + state: reviewed, + gitInfo: cleanGit, + from: 'REVIEWING', + to: 'IMPLEMENTING', + }), + ).toMatchObject({ pass: false, reason: expect.stringContaining('All reviews passed') }) + }) + + it('requires a recorded pull request before awaiting CI', () => { + const state = getInitialWorkflowState().with({ currentStateMachineState: 'SUBMITTING_PR' }) + + expect( + submittingPrGuard({ state, gitInfo: cleanGit, from: 'SUBMITTING_PR', to: 'AWAITING_CI' }), + ).toMatchObject({ pass: false, reason: expect.stringContaining('prNumber not set') }) + expect( + submittingPrGuard({ + state: state.with({ prNumber: 42 }), + gitInfo: cleanGit, + from: 'SUBMITTING_PR', + to: 'AWAITING_CI', + }), + ).toStrictEqual({ pass: true }) + }) +}) diff --git a/packages/dev-workflow-v2/domain-model/src/domain/states/addressing-feedback.ts b/packages/dev-workflow-v2/domain-model/src/domain/states/addressing-feedback.ts new file mode 100644 index 000000000..89cf6b23b --- /dev/null +++ b/packages/dev-workflow-v2/domain-model/src/domain/states/addressing-feedback.ts @@ -0,0 +1,31 @@ +import type { WorkflowState } from '../workflow-types' +import { defineState } from '../define-state' +import { pass, fail } from '@nt-ai-lab/deterministic-agent-workflow-dsl' + +/** @riviere-role domain-service */ +export function defineAddressingFeedbackState() { + return defineState({ + emoji: '🔧', + agentInstructions: 'states/addressing_feedback.md', + canTransitionTo: ['REVIEWING', 'BLOCKED'], + allowedWorkflowOperations: ['verify-feedback-addressed'], + forbidden: { write: true }, + + transitionGuard: (ctx) => { + if (ctx.to === 'BLOCKED') return pass() + if (!ctx.state.feedbackAddressed) + return fail('Feedback not addressed. Run verify-feedback-addressed first.') + if (!ctx.state.feedbackClean) + return fail( + 'PR feedback is not yet clear. Resolve all feedback, ensure no CHANGES_REQUESTED review remains, then run verify-feedback-addressed again.', + ) + return pass() + }, + + onEntry: (state: WorkflowState): WorkflowState => + state.with({ + feedbackAddressed: false, + feedbackClean: false, + }), + }) +} diff --git a/packages/dev-workflow-v2/domain-model/src/domain/states/awaiting-ci.ts b/packages/dev-workflow-v2/domain-model/src/domain/states/awaiting-ci.ts new file mode 100644 index 000000000..d10247b2c --- /dev/null +++ b/packages/dev-workflow-v2/domain-model/src/domain/states/awaiting-ci.ts @@ -0,0 +1,22 @@ +import { defineState } from '../define-state' +import { pass, fail } from '@nt-ai-lab/deterministic-agent-workflow-dsl' + +/** @riviere-role domain-service */ +export function defineAwaitingCiState() { + return defineState({ + emoji: '⏳', + agentInstructions: 'states/awaiting_ci.md', + canTransitionTo: ['AWAITING_PR_FEEDBACK', 'IMPLEMENTING', 'BLOCKED'], + allowedWorkflowOperations: ['record-ci-passed', 'record-ci-failed'], + forbidden: { write: true }, + allowForbidden: { bash: ['gh pr checks'] }, + + transitionGuard: (ctx) => { + if (ctx.to === 'AWAITING_PR_FEEDBACK' && !ctx.state.ciPassed) + return fail('CI not passed. Run record-ci-passed first.') + if (ctx.to === 'IMPLEMENTING' && ctx.state.ciPassed) + return fail('CI passed. Transition to AWAITING_PR_FEEDBACK, not IMPLEMENTING.') + return pass() + }, + }) +} diff --git a/packages/dev-workflow-v2/domain-model/src/domain/states/awaiting-pr-feedback.ts b/packages/dev-workflow-v2/domain-model/src/domain/states/awaiting-pr-feedback.ts new file mode 100644 index 000000000..8d26ef064 --- /dev/null +++ b/packages/dev-workflow-v2/domain-model/src/domain/states/awaiting-pr-feedback.ts @@ -0,0 +1,12 @@ +import { defineState } from '../define-state' + +/** @riviere-role domain-service */ +export function defineAwaitingPrFeedbackState() { + return defineState({ + emoji: '💬', + agentInstructions: 'states/awaiting_pr_feedback.md', + canTransitionTo: ['ADDRESSING_FEEDBACK', 'REFLECTING'], + allowedWorkflowOperations: [], + forbidden: { write: true }, + }) +} diff --git a/packages/dev-workflow-v2/domain-model/src/domain/states/blocked.ts b/packages/dev-workflow-v2/domain-model/src/domain/states/blocked.ts new file mode 100644 index 000000000..42f238cf6 --- /dev/null +++ b/packages/dev-workflow-v2/domain-model/src/domain/states/blocked.ts @@ -0,0 +1,33 @@ +import { defineState } from '../define-state' +import { pass, fail } from '@nt-ai-lab/deterministic-agent-workflow-dsl' + +/** @riviere-role domain-service */ +export function defineBlockedState() { + return defineState({ + emoji: '⚠️', + agentInstructions: 'states/blocked.md', + allowIdle: true, + forbidden: { write: true }, + canTransitionTo: [ + 'IMPLEMENTING', + 'REVIEWING', + 'SUBMITTING_PR', + 'AWAITING_CI', + 'AWAITING_PR_FEEDBACK', + 'ADDRESSING_FEEDBACK', + 'REFLECTING', + ], + allowedWorkflowOperations: [], + + transitionGuard: (ctx) => { + const preBlockedState = ctx.state.preBlockedState + if (ctx.to !== preBlockedState) { + /* v8 ignore next 4 */ + return fail( + `Cannot transition from BLOCKED to ${ctx.to}. Must return to pre-blocked state: ${preBlockedState ?? 'unknown'}.`, + ) + } + return pass() + }, + }) +} diff --git a/packages/dev-workflow-v2/domain-model/src/domain/states/complete.ts b/packages/dev-workflow-v2/domain-model/src/domain/states/complete.ts new file mode 100644 index 000000000..77a30e4f3 --- /dev/null +++ b/packages/dev-workflow-v2/domain-model/src/domain/states/complete.ts @@ -0,0 +1,13 @@ +import { defineState } from '../define-state' + +/** @riviere-role domain-service */ +export function defineCompleteState() { + return defineState({ + emoji: '✅', + agentInstructions: 'states/complete.md', + allowIdle: true, + canTransitionTo: [], + allowedWorkflowOperations: [], + forbidden: { write: true }, + }) +} diff --git a/packages/dev-workflow-v2/domain-model/src/domain/states/implementing.ts b/packages/dev-workflow-v2/domain-model/src/domain/states/implementing.ts new file mode 100644 index 000000000..3a3a2201d --- /dev/null +++ b/packages/dev-workflow-v2/domain-model/src/domain/states/implementing.ts @@ -0,0 +1,36 @@ +import type { WorkflowState } from '../workflow-types' +import { defineState } from '../define-state' +import { pass, fail } from '@nt-ai-lab/deterministic-agent-workflow-dsl' + +/** @riviere-role domain-service */ +export function defineImplementingState() { + return defineState({ + emoji: '🔨', + agentInstructions: 'states/implementing.md', + canTransitionTo: ['REVIEWING', 'BLOCKED'], + allowedWorkflowOperations: ['record-issue', 'record-branch'], + forbidden: { write: true }, + + transitionGuard: (ctx) => { + /* v8 ignore next */ + if (ctx.to === 'BLOCKED') return pass() + if (!ctx.gitInfo.hasCommitsVsDefault) + return fail('No commits beyond default branch. Write code and commit before reviewing.') + if (!ctx.gitInfo.workingTreeClean) + return fail('Working tree is not clean. Commit all changes before transitioning.') + if (!ctx.state.githubIssue) return fail('No issue recorded. Run record-issue first.') + return pass() + }, + + onEntry: (state: WorkflowState): WorkflowState => + state.with({ + architectureReviewPassed: false, + codeReviewPassed: false, + bugScannerPassed: false, + taskCheckPassed: false, + ciPassed: false, + feedbackClean: false, + feedbackAddressed: false, + }), + }) +} diff --git a/packages/dev-workflow-v2/domain-model/src/domain/states/reflecting.ts b/packages/dev-workflow-v2/domain-model/src/domain/states/reflecting.ts new file mode 100644 index 000000000..40ac7d17f --- /dev/null +++ b/packages/dev-workflow-v2/domain-model/src/domain/states/reflecting.ts @@ -0,0 +1,12 @@ +import { defineState } from '../define-state' + +/** @riviere-role domain-service */ +export function defineReflectingState() { + return defineState({ + emoji: '🪞', + agentInstructions: 'states/reflecting.md', + canTransitionTo: ['COMPLETE', 'BLOCKED'], + allowedWorkflowOperations: [], + forbidden: { write: true }, + }) +} diff --git a/packages/dev-workflow-v2/domain-model/src/domain/states/reviewing.ts b/packages/dev-workflow-v2/domain-model/src/domain/states/reviewing.ts new file mode 100644 index 000000000..3e245c3a1 --- /dev/null +++ b/packages/dev-workflow-v2/domain-model/src/domain/states/reviewing.ts @@ -0,0 +1,32 @@ +import { defineState } from '../define-state' +import { pass, fail } from '@nt-ai-lab/deterministic-agent-workflow-dsl' + +/** @riviere-role domain-service */ +export function defineReviewingState() { + return defineState({ + emoji: '📋', + agentInstructions: 'states/reviewing.md', + canTransitionTo: ['SUBMITTING_PR', 'IMPLEMENTING', 'BLOCKED'], + forbidden: { write: true }, + allowedWorkflowOperations: ['record-review'], + + transitionGuard: (ctx) => { + const taskCheckRequired = ctx.state.githubIssue !== undefined + const allPassed = + ctx.state.architectureReviewPassed && + ctx.state.codeReviewPassed && + ctx.state.bugScannerPassed && + (!taskCheckRequired || ctx.state.taskCheckPassed) + + if (ctx.to === 'SUBMITTING_PR' && !allPassed) + return fail( + taskCheckRequired + ? 'Not all reviews passed. Each of architecture-review, code-review, bug-scanner, and task-check must pass.' + : 'Not all reviews passed. Each of architecture-review, code-review, and bug-scanner must pass.', + ) + if (ctx.to === 'IMPLEMENTING' && allPassed) + return fail('All reviews passed. Transition to SUBMITTING_PR, not IMPLEMENTING.') + return pass() + }, + }) +} diff --git a/packages/dev-workflow-v2/domain-model/src/domain/states/submitting-pr.ts b/packages/dev-workflow-v2/domain-model/src/domain/states/submitting-pr.ts new file mode 100644 index 000000000..ed23304ca --- /dev/null +++ b/packages/dev-workflow-v2/domain-model/src/domain/states/submitting-pr.ts @@ -0,0 +1,20 @@ +import { defineState } from '../define-state' +import { pass, fail } from '@nt-ai-lab/deterministic-agent-workflow-dsl' + +/** @riviere-role domain-service */ +export function defineSubmittingPrState() { + return defineState({ + emoji: '🚀', + agentInstructions: 'states/submitting_pr.md', + canTransitionTo: ['AWAITING_CI', 'BLOCKED'], + allowedWorkflowOperations: ['record-pr', 'create-pr'], + forbidden: { write: true }, + + allowForbidden: { bash: ['git push'] }, + + transitionGuard: (ctx) => { + if (!ctx.state.prNumber) return fail('prNumber not set. Run record-pr first.') + return pass() + }, + }) +} diff --git a/tools/dev-workflow-v2/src/features/workflow/domain/workflow-addressing-feedback.spec.ts b/packages/dev-workflow-v2/domain-model/src/domain/workflow-addressing-feedback.spec.ts similarity index 94% rename from tools/dev-workflow-v2/src/features/workflow/domain/workflow-addressing-feedback.spec.ts rename to packages/dev-workflow-v2/domain-model/src/domain/workflow-addressing-feedback.spec.ts index de92528cc..0746b8238 100644 --- a/tools/dev-workflow-v2/src/features/workflow/domain/workflow-addressing-feedback.spec.ts +++ b/packages/dev-workflow-v2/domain-model/src/domain/workflow-addressing-feedback.spec.ts @@ -1,12 +1,13 @@ -import { - describe, it, expect -} from 'vitest' +import { describe, it, expect } from 'vitest' import { spec, eventsToAddressingFeedback, unresolvedThread, -} from './fixtures/workflow-test-fixtures' -import { addressingFeedbackState } from './states/addressing-feedback' +} from './__fixtures__/workflow-test-fixtures' +import { defineAddressingFeedbackState } from './states/addressing-feedback' + +const addressingFeedbackState = defineAddressingFeedbackState() +import { WorkflowState } from './workflow-types' function addressingTransitionGuard(): NonNullable { const guard = addressingFeedbackState.transitionGuard @@ -149,7 +150,7 @@ describe('ADDRESSING_FEEDBACK workflow behavior', () => { const guard = addressingTransitionGuard() const entered = spec.given(...eventsToAddressingFeedback()).when((wf) => wf.getState()) const guardResult = guard({ - state: { + state: WorkflowState.parse({ currentStateMachineState: 'ADDRESSING_FEEDBACK', architectureReviewPassed: false, codeReviewPassed: false, @@ -158,7 +159,7 @@ describe('ADDRESSING_FEEDBACK workflow behavior', () => { ciPassed: false, feedbackClean: false, feedbackAddressed: true, - }, + }), gitInfo: { currentBranch: 'issue-42', workingTreeClean: true, @@ -183,7 +184,7 @@ describe('ADDRESSING_FEEDBACK workflow behavior', () => { it('allows transition to BLOCKED even when feedback is not yet addressed', () => { const guard = addressingTransitionGuard() const guardResult = guard({ - state: { + state: WorkflowState.parse({ currentStateMachineState: 'ADDRESSING_FEEDBACK', architectureReviewPassed: false, codeReviewPassed: false, @@ -192,7 +193,7 @@ describe('ADDRESSING_FEEDBACK workflow behavior', () => { ciPassed: false, feedbackClean: false, feedbackAddressed: false, - }, + }), gitInfo: { currentBranch: 'issue-42', workingTreeClean: true, diff --git a/tools/dev-workflow-v2/src/features/workflow/domain/workflow-event-types.spec.ts b/packages/dev-workflow-v2/domain-model/src/domain/workflow-event-types.spec.ts similarity index 100% rename from tools/dev-workflow-v2/src/features/workflow/domain/workflow-event-types.spec.ts rename to packages/dev-workflow-v2/domain-model/src/domain/workflow-event-types.spec.ts diff --git a/packages/dev-workflow-v2/domain-model/src/domain/workflow-events.spec.ts b/packages/dev-workflow-v2/domain-model/src/domain/workflow-events.spec.ts new file mode 100644 index 000000000..6fc5c343b --- /dev/null +++ b/packages/dev-workflow-v2/domain-model/src/domain/workflow-events.spec.ts @@ -0,0 +1,445 @@ +import { parseWorkflowEvent, type WorkflowEvent } from './workflow-events' + +const AT = '2026-01-01T00:00:00Z' + +describe('parseWorkflowEvent — session-started', () => { + it('accepts valid payload', () => { + const result: WorkflowEvent = parseWorkflowEvent({ + type: 'session-started', + at: AT, + }) + expect(result.type).toStrictEqual('session-started') + }) + + it('accepts optional repository', () => { + const result = parseWorkflowEvent({ + type: 'session-started', + at: AT, + repository: 'owner/repo', + }) + expect(result.type).toStrictEqual('session-started') + }) +}) + +describe('parseWorkflowEvent — issue-recorded', () => { + it('accepts valid payload', () => { + const result = parseWorkflowEvent({ + type: 'issue-recorded', + at: AT, + issueNumber: 42, + }) + expect(result.type).toStrictEqual('issue-recorded') + }) + + it('rejects missing issueNumber', () => { + expect(() => + parseWorkflowEvent({ + type: 'issue-recorded', + at: AT, + }), + ).toThrow('Required') + }) +}) + +describe('parseWorkflowEvent — branch-recorded', () => { + it('accepts valid payload', () => { + const result = parseWorkflowEvent({ + type: 'branch-recorded', + at: AT, + branch: 'feature/foo', + }) + expect(result.type).toStrictEqual('branch-recorded') + }) + + it('rejects missing branch', () => { + expect(() => + parseWorkflowEvent({ + type: 'branch-recorded', + at: AT, + }), + ).toThrow('Required') + }) +}) + +describe('parseWorkflowEvent — architecture-review-completed', () => { + it('accepts passed payload', () => { + const result = parseWorkflowEvent({ + type: 'architecture-review-completed', + at: AT, + passed: true, + }) + expect(result.type).toStrictEqual('architecture-review-completed') + }) + + it('rejects missing passed', () => { + expect(() => + parseWorkflowEvent({ + type: 'architecture-review-completed', + at: AT, + }), + ).toThrow('Required') + }) +}) + +describe('parseWorkflowEvent — code-review-completed', () => { + it('accepts passed payload', () => { + const result = parseWorkflowEvent({ + type: 'code-review-completed', + at: AT, + passed: true, + }) + expect(result.type).toStrictEqual('code-review-completed') + }) + + it('rejects missing passed', () => { + expect(() => + parseWorkflowEvent({ + type: 'code-review-completed', + at: AT, + }), + ).toThrow('Required') + }) +}) + +describe('parseWorkflowEvent — bug-scanner-completed', () => { + it('accepts passed payload', () => { + const result = parseWorkflowEvent({ + type: 'bug-scanner-completed', + at: AT, + passed: true, + }) + expect(result.type).toStrictEqual('bug-scanner-completed') + }) + + it('rejects missing passed', () => { + expect(() => + parseWorkflowEvent({ + type: 'bug-scanner-completed', + at: AT, + }), + ).toThrow('Required') + }) +}) + +describe('parseWorkflowEvent — pr-recorded', () => { + it('accepts valid payload', () => { + const result = parseWorkflowEvent({ + type: 'pr-recorded', + at: AT, + prNumber: 7, + }) + expect(result.type).toStrictEqual('pr-recorded') + }) + + it('accepts optional prUrl', () => { + const result = parseWorkflowEvent({ + type: 'pr-recorded', + at: AT, + prNumber: 7, + prUrl: 'https://github.com/x/y/pull/7', + }) + expect(result.type).toStrictEqual('pr-recorded') + }) + + it('rejects missing prNumber', () => { + expect(() => + parseWorkflowEvent({ + type: 'pr-recorded', + at: AT, + }), + ).toThrow('Required') + }) +}) + +describe('parseWorkflowEvent — ci-completed', () => { + it('accepts passed payload', () => { + const result = parseWorkflowEvent({ + type: 'ci-completed', + at: AT, + passed: true, + }) + expect(result.type).toStrictEqual('ci-completed') + }) + + it('accepts failed payload with output', () => { + const result = parseWorkflowEvent({ + type: 'ci-completed', + at: AT, + passed: false, + output: 'test failures', + }) + expect(result.type).toStrictEqual('ci-completed') + }) + + it('rejects missing passed', () => { + expect(() => + parseWorkflowEvent({ + type: 'ci-completed', + at: AT, + }), + ).toThrow('Required') + }) +}) + +describe('parseWorkflowEvent — feedback-checked', () => { + it('accepts clean payload', () => { + const result = parseWorkflowEvent({ + type: 'feedback-checked', + at: AT, + clean: true, + }) + expect(result.type).toStrictEqual('feedback-checked') + }) + + it('accepts dirty payload with unresolvedCount', () => { + const result = parseWorkflowEvent({ + type: 'feedback-checked', + at: AT, + clean: false, + unresolvedCount: 3, + reviewDecision: 'CHANGES_REQUESTED', + }) + expect(result.type).toStrictEqual('feedback-checked') + }) + + it('accepts dirty payload with null reviewDecision', () => { + const result = parseWorkflowEvent({ + type: 'feedback-checked', + at: AT, + clean: false, + unresolvedCount: 0, + reviewDecision: null, + }) + expect(result.type).toStrictEqual('feedback-checked') + }) + + it('rejects missing clean', () => { + expect(() => + parseWorkflowEvent({ + type: 'feedback-checked', + at: AT, + }), + ).toThrow('Required') + }) +}) + +describe('parseWorkflowEvent — feedback-addressed', () => { + it('accepts valid payload', () => { + const result = parseWorkflowEvent({ + type: 'feedback-addressed', + at: AT, + }) + expect(result.type).toStrictEqual('feedback-addressed') + }) + + it('rejects missing at', () => { + const malformedEvent = { type: 'feedback-addressed', at: AT } + Reflect.deleteProperty(malformedEvent, 'at') + + expect(() => parseWorkflowEvent(malformedEvent)).toThrow('Required') + }) +}) + +describe('parseWorkflowEvent — task-check-passed', () => { + it('accepts valid payload', () => { + const result = parseWorkflowEvent({ + type: 'task-check-passed', + at: AT, + }) + expect(result.type).toStrictEqual('task-check-passed') + }) + + it('rejects missing at', () => { + const malformedEvent = { type: 'task-check-passed', at: AT } + Reflect.deleteProperty(malformedEvent, 'at') + + expect(() => parseWorkflowEvent(malformedEvent)).toThrow('Required') + }) +}) + +describe('parseWorkflowEvent — review-recorded', () => { + it('accepts pass verdict payload', () => { + const result = parseWorkflowEvent({ + type: 'review-recorded', + at: AT, + reviewId: 1, + reviewType: 'task-check', + verdict: 'PASS', + }) + expect(result.type).toStrictEqual('review-recorded') + }) + + it('accepts fail verdict payload', () => { + const result = parseWorkflowEvent({ + type: 'review-recorded', + at: AT, + reviewId: 2, + reviewType: 'code-review', + verdict: 'FAIL', + }) + expect(result.type).toStrictEqual('review-recorded') + }) + + it('rejects missing reviewType', () => { + expect(() => + parseWorkflowEvent({ + type: 'review-recorded', + at: AT, + reviewId: 1, + verdict: 'PASS', + }), + ).toThrow('Required') + }) + + it('rejects unknown verdict', () => { + expect(() => + parseWorkflowEvent({ + type: 'review-recorded', + at: AT, + reviewId: 1, + reviewType: 'task-check', + verdict: 'MAYBE', + }), + ).toThrow('Invalid enum value') + }) +}) + +describe('parseWorkflowEvent — bash-checked', () => { + it('accepts valid payload', () => { + const result = parseWorkflowEvent({ + type: 'bash-checked', + at: AT, + tool: 'Bash', + command: 'pnpm test', + allowed: true, + }) + expect(result.type).toStrictEqual('bash-checked') + }) + + it('accepts optional reason', () => { + const result = parseWorkflowEvent({ + type: 'bash-checked', + at: AT, + tool: 'Bash', + command: 'git push', + allowed: false, + reason: 'forbidden', + }) + expect(result.type).toStrictEqual('bash-checked') + }) + + it('rejects missing command', () => { + expect(() => + parseWorkflowEvent({ + type: 'bash-checked', + at: AT, + tool: 'Bash', + allowed: true, + }), + ).toThrow('Required') + }) +}) + +describe('parseWorkflowEvent — write-checked', () => { + it('accepts valid payload', () => { + const result = parseWorkflowEvent({ + type: 'write-checked', + at: AT, + tool: 'Write', + filePath: '/test-output/x.ts', + allowed: true, + }) + expect(result.type).toStrictEqual('write-checked') + }) + + it('accepts optional reason', () => { + const result = parseWorkflowEvent({ + type: 'write-checked', + at: AT, + tool: 'Write', + filePath: '/test-output/x.ts', + allowed: false, + reason: 'blocked', + }) + expect(result.type).toStrictEqual('write-checked') + }) + + it('rejects missing filePath', () => { + expect(() => + parseWorkflowEvent({ + type: 'write-checked', + at: AT, + tool: 'Write', + allowed: true, + }), + ).toThrow('Required') + }) +}) + +describe('parseWorkflowEvent — transitioned', () => { + it('accepts valid payload', () => { + const result = parseWorkflowEvent({ + type: 'transitioned', + at: AT, + from: 'IMPLEMENTING', + to: 'REVIEWING', + }) + expect(result.type).toStrictEqual('transitioned') + }) + + it('accepts optional preBlockedState', () => { + const result = parseWorkflowEvent({ + type: 'transitioned', + at: AT, + from: 'IMPLEMENTING', + to: 'BLOCKED', + preBlockedState: 'IMPLEMENTING', + }) + expect(result.type).toStrictEqual('transitioned') + }) + + it('rejects missing from', () => { + expect(() => + parseWorkflowEvent({ + type: 'transitioned', + at: AT, + to: 'REVIEWING', + }), + ).toThrow('Required') + }) + + it('rejects missing to', () => { + expect(() => + parseWorkflowEvent({ + type: 'transitioned', + at: AT, + from: 'IMPLEMENTING', + }), + ).toThrow('Required') + }) +}) + +describe('parseWorkflowEvent — discriminant validation', () => { + it('rejects unknown type discriminant', () => { + expect(() => + parseWorkflowEvent({ + type: 'unknown-event', + at: AT, + }), + ).toThrow('Invalid discriminator value') + }) + + it('rejects missing type field', () => { + const malformedEvent = { type: 'session-started', at: AT } + Reflect.deleteProperty(malformedEvent, 'type') + + expect(() => parseWorkflowEvent(malformedEvent)).toThrow('Invalid discriminator value') + }) + + it('rejects missing at when type is present', () => { + const malformedEvent = { type: 'session-started', at: AT } + Reflect.deleteProperty(malformedEvent, 'at') + + expect(() => parseWorkflowEvent(malformedEvent)).toThrow('Required') + }) +}) diff --git a/tools/dev-workflow-v2/src/features/workflow/domain/workflow-events.ts b/packages/dev-workflow-v2/domain-model/src/domain/workflow-events.ts similarity index 95% rename from tools/dev-workflow-v2/src/features/workflow/domain/workflow-events.ts rename to packages/dev-workflow-v2/domain-model/src/domain/workflow-events.ts index f50d2542d..51307a85d 100644 --- a/tools/dev-workflow-v2/src/features/workflow/domain/workflow-events.ts +++ b/packages/dev-workflow-v2/domain-model/src/domain/workflow-events.ts @@ -1,6 +1,8 @@ import { z } from 'zod' import type { BaseEvent } from '@nt-ai-lab/deterministic-agent-workflow-engine' -import { STATE_NAME_SCHEMA } from './workflow-types' +import { getWorkflowStateNameSchema } from './workflow-types' + +const STATE_NAME_SCHEMA = getWorkflowStateNameSchema() const KNOWN_WORKFLOW_EVENT_TYPES = [ 'session-started', @@ -128,7 +130,7 @@ const WRITE_CHECKED_SCHEMA = z.object({ reason: z.string().optional(), }) -export const WORKFLOW_EVENT_SCHEMA = z.discriminatedUnion('type', [ +const WORKFLOW_EVENT_SCHEMA = z.discriminatedUnion('type', [ SESSION_STARTED_SCHEMA, TRANSITIONED_SCHEMA, ISSUE_RECORDED_SCHEMA, diff --git a/tools/dev-workflow-v2/src/features/workflow/domain/workflow-feedback-reflecting.spec.ts b/packages/dev-workflow-v2/domain-model/src/domain/workflow-feedback-reflecting.spec.ts similarity index 98% rename from tools/dev-workflow-v2/src/features/workflow/domain/workflow-feedback-reflecting.spec.ts rename to packages/dev-workflow-v2/domain-model/src/domain/workflow-feedback-reflecting.spec.ts index 5a1daf1f3..b9ccc9d22 100644 --- a/tools/dev-workflow-v2/src/features/workflow/domain/workflow-feedback-reflecting.spec.ts +++ b/packages/dev-workflow-v2/domain-model/src/domain/workflow-feedback-reflecting.spec.ts @@ -1,11 +1,9 @@ -import { - describe, it, expect, vi -} from 'vitest' +import { describe, it, expect, vi } from 'vitest' import { makeDeps, eventsToAwaitingPrFeedback, unresolvedThread, -} from './fixtures/workflow-test-fixtures' +} from './__fixtures__/workflow-test-fixtures' import { Workflow } from './workflow' import { applyEvents } from './fold' diff --git a/tools/dev-workflow-v2/src/features/workflow/domain/workflow-hook-checks.spec.ts b/packages/dev-workflow-v2/domain-model/src/domain/workflow-hook-checks.spec.ts similarity index 88% rename from tools/dev-workflow-v2/src/features/workflow/domain/workflow-hook-checks.spec.ts rename to packages/dev-workflow-v2/domain-model/src/domain/workflow-hook-checks.spec.ts index 7d4684f1c..969090686 100644 --- a/tools/dev-workflow-v2/src/features/workflow/domain/workflow-hook-checks.spec.ts +++ b/packages/dev-workflow-v2/domain-model/src/domain/workflow-hook-checks.spec.ts @@ -1,9 +1,7 @@ -import { - checkWriteAllowed, isWriteAllowed -} from './workflow-predicates' -import type { WorkflowState } from './workflow-types' +import { checkWriteAllowed, isWriteAllowed } from './workflow-predicates' +import { WorkflowState } from './workflow-types' -const BASE_STATE: WorkflowState = { +const BASE_STATE = WorkflowState.parse({ currentStateMachineState: 'IMPLEMENTING', architectureReviewPassed: false, codeReviewPassed: false, @@ -12,7 +10,7 @@ const BASE_STATE: WorkflowState = { ciPassed: false, feedbackClean: false, feedbackAddressed: false, -} +}) describe('checkWriteAllowed predicate', () => { it('allows writes to normal files', () => { diff --git a/tools/dev-workflow-v2/src/features/workflow/domain/workflow-implementing.spec.ts b/packages/dev-workflow-v2/domain-model/src/domain/workflow-implementing.spec.ts similarity index 93% rename from tools/dev-workflow-v2/src/features/workflow/domain/workflow-implementing.spec.ts rename to packages/dev-workflow-v2/domain-model/src/domain/workflow-implementing.spec.ts index 0af922b65..39f003a21 100644 --- a/tools/dev-workflow-v2/src/features/workflow/domain/workflow-implementing.spec.ts +++ b/packages/dev-workflow-v2/domain-model/src/domain/workflow-implementing.spec.ts @@ -5,7 +5,7 @@ import { transitioned, eventsToReviewing, codeReviewFailed, -} from './fixtures/workflow-test-fixtures' +} from './__fixtures__/workflow-test-fixtures' describe('Workflow', () => { describe('createFresh', () => { @@ -61,9 +61,7 @@ describe('Workflow', () => { describe('registerAgent', () => { it('returns pass (no-op for single-agent workflow)', () => { - const { - result, events - } = spec.given().when((wf) => wf.registerAgent('lead', 'agent-1')) + const { result, events } = spec.given().when((wf) => wf.registerAgent('lead', 'agent-1')) expect(result).toStrictEqual({ pass: true }) expect(events).toHaveLength(0) }) @@ -71,9 +69,7 @@ describe('Workflow', () => { describe('handleTeammateIdle', () => { it('returns pass (no-op for single-agent workflow)', () => { - const { - result, events - } = spec.given().when((wf) => wf.handleTeammateIdle('agent-1')) + const { result, events } = spec.given().when((wf) => wf.handleTeammateIdle('agent-1')) expect(result).toStrictEqual({ pass: true }) expect(events).toHaveLength(0) }) @@ -81,9 +77,7 @@ describe('Workflow', () => { describe('IMPLEMENTING state', () => { it('sets githubIssue when record-issue succeeds', () => { - const { - result, state, events - } = spec + const { result, state, events } = spec .given() .when((wf) => wf.executeRecording('record-issue', 42)) expect(result).toStrictEqual({ pass: true }) @@ -106,9 +100,7 @@ describe('Workflow', () => { }) it('sets featureBranch when record-branch succeeds', () => { - const { - result, state, events - } = spec + const { result, state, events } = spec .given() .when((wf) => wf.executeRecording('record-branch', 'feature/x')) expect(result).toStrictEqual({ pass: true }) diff --git a/packages/dev-workflow-v2/domain-model/src/domain/workflow-pr-feedback-verification-events.spec.ts b/packages/dev-workflow-v2/domain-model/src/domain/workflow-pr-feedback-verification-events.spec.ts new file mode 100644 index 000000000..6c5a8b863 --- /dev/null +++ b/packages/dev-workflow-v2/domain-model/src/domain/workflow-pr-feedback-verification-events.spec.ts @@ -0,0 +1,28 @@ +import { parseWorkflowEvent } from './workflow-events' + +const AT = '2026-01-01T00:00:00Z' + +describe('parseWorkflowEvent — pr-feedback-verification-failed', () => { + it('accepts failure reason payload', () => { + const result = parseWorkflowEvent({ + type: 'pr-feedback-verification-failed', + at: AT, + reason: 'CodeRabbit feedback did not appear.', + }) + + expect(result).toStrictEqual({ + type: 'pr-feedback-verification-failed', + at: AT, + reason: 'CodeRabbit feedback did not appear.', + }) + }) + + it('rejects missing reason', () => { + expect(() => + parseWorkflowEvent({ + type: 'pr-feedback-verification-failed', + at: AT, + }), + ).toThrow('Required') + }) +}) diff --git a/tools/dev-workflow-v2/src/features/workflow/domain/workflow-predicates.ts b/packages/dev-workflow-v2/domain-model/src/domain/workflow-predicates.ts similarity index 100% rename from tools/dev-workflow-v2/src/features/workflow/domain/workflow-predicates.ts rename to packages/dev-workflow-v2/domain-model/src/domain/workflow-predicates.ts diff --git a/tools/dev-workflow-v2/src/features/workflow/domain/workflow-reviewing-submitting.spec.ts b/packages/dev-workflow-v2/domain-model/src/domain/workflow-reviewing-submitting.spec.ts similarity index 96% rename from tools/dev-workflow-v2/src/features/workflow/domain/workflow-reviewing-submitting.spec.ts rename to packages/dev-workflow-v2/domain-model/src/domain/workflow-reviewing-submitting.spec.ts index 1c2eb3a21..5a43cc207 100644 --- a/tools/dev-workflow-v2/src/features/workflow/domain/workflow-reviewing-submitting.spec.ts +++ b/packages/dev-workflow-v2/domain-model/src/domain/workflow-reviewing-submitting.spec.ts @@ -7,10 +7,12 @@ import { eventsToAwaitingCi, makeDeps, reviewRecorded, -} from './fixtures/workflow-test-fixtures' +} from './__fixtures__/workflow-test-fixtures' import { Workflow } from './workflow' import { applyEvents } from './fold' -import { reviewingState } from './states/reviewing' +import { defineReviewingState } from './states/reviewing' + +const reviewingState = defineReviewingState() const CREATE_PR_OPTIONS = [ '--title', @@ -39,10 +41,7 @@ function getReviewingTransitionGuard(): NonNullable { it('rejects SUBMITTING_PR without task check when no issue is recorded and required reviews failed', () => { const result = getReviewingTransitionGuard()({ - state: { - ...Workflow.createFresh(makeDeps()).getState(), + state: Workflow.createFresh(makeDeps()).getState().with({ currentStateMachineState: 'REVIEWING', architectureReviewPassed: false, codeReviewPassed: false, bugScannerPassed: false, - }, + }), gitInfo: makeDeps().getGitInfo(), from: 'REVIEWING', to: 'SUBMITTING_PR', @@ -167,9 +165,7 @@ describe('Workflow', () => { describe('SUBMITTING_PR state', () => { it('records PR number with URL', () => { - const { - result, state, events - } = spec + const { result, state, events } = spec .given(...eventsToSubmittingPr()) .when((wf) => wf.executeRecording('record-pr', 99, 'https://github.com/x/y/pull/99')) expect(result).toStrictEqual({ pass: true }) @@ -186,9 +182,7 @@ describe('Workflow', () => { }) it('records PR number without URL', () => { - const { - result, state, events - } = spec + const { result, state, events } = spec .given(...eventsToSubmittingPr()) .when((wf) => wf.executeRecording('record-pr', 99)) expect(result).toStrictEqual({ pass: true }) @@ -357,9 +351,7 @@ describe('Workflow', () => { describe('AWAITING_CI state', () => { it('records CI passed', () => { - const { - result, state - } = spec + const { result, state } = spec .given(...eventsToAwaitingCi()) .when((wf) => wf.executeRecording('record-ci-passed')) expect(result).toStrictEqual({ pass: true }) @@ -367,9 +359,7 @@ describe('Workflow', () => { }) it('records CI failed', () => { - const { - result, state - } = spec + const { result, state } = spec .given(...eventsToAwaitingCi()) .when((wf) => wf.executeRecording('record-ci-failed', 'test failures')) expect(result).toStrictEqual({ pass: true }) diff --git a/tools/dev-workflow-v2/src/features/workflow/domain/workflow-types.spec.ts b/packages/dev-workflow-v2/domain-model/src/domain/workflow-types.spec.ts similarity index 94% rename from tools/dev-workflow-v2/src/features/workflow/domain/workflow-types.spec.ts rename to packages/dev-workflow-v2/domain-model/src/domain/workflow-types.spec.ts index 62c464224..31537fa89 100644 --- a/tools/dev-workflow-v2/src/features/workflow/domain/workflow-types.spec.ts +++ b/packages/dev-workflow-v2/domain-model/src/domain/workflow-types.spec.ts @@ -1,7 +1,11 @@ import { - createWorkflowStateSchema, STATE_NAME_SCHEMA, STATE_NAMES + createWorkflowStateSchema, + getWorkflowStateNameSchema, + getWorkflowStateNames, } from './workflow-types' +const STATE_NAMES = getWorkflowStateNames() +const STATE_NAME_SCHEMA = getWorkflowStateNameSchema() const workflowStateSchema = createWorkflowStateSchema(STATE_NAMES) describe('STATE_NAME_SCHEMA', () => { diff --git a/packages/dev-workflow-v2/domain-model/src/domain/workflow-types.ts b/packages/dev-workflow-v2/domain-model/src/domain/workflow-types.ts new file mode 100644 index 000000000..ce7768d96 --- /dev/null +++ b/packages/dev-workflow-v2/domain-model/src/domain/workflow-types.ts @@ -0,0 +1,129 @@ +import { z } from 'zod' + +const STATE_NAMES = [ + 'IMPLEMENTING', + 'REVIEWING', + 'SUBMITTING_PR', + 'AWAITING_CI', + 'AWAITING_PR_FEEDBACK', + 'ADDRESSING_FEEDBACK', + 'REFLECTING', + 'COMPLETE', + 'BLOCKED', +] as const + +type StateName = (typeof STATE_NAMES)[number] + +const STATE_NAME_SCHEMA = z.enum(STATE_NAMES) + +/** @riviere-role domain-service */ +export function createWorkflowStateSchema(stateNames: T) { + const stateNameSchema = z.enum(stateNames) + return z.object({ + currentStateMachineState: stateNameSchema, + githubIssue: z.number().int().positive().optional(), + featureBranch: z.string().optional(), + prNumber: z.number().int().positive().optional(), + prUrl: z.string().optional(), + architectureReviewPassed: z.boolean(), + codeReviewPassed: z.boolean(), + bugScannerPassed: z.boolean(), + taskCheckPassed: z.boolean(), + ciPassed: z.boolean(), + feedbackClean: z.boolean(), + feedbackAddressed: z.boolean(), + feedbackUnresolvedCount: z.number().optional(), + prFeedbackVerificationFailedReason: z.string().optional(), + preBlockedState: z.string().optional(), + transcriptPath: z.string().optional(), + }) +} + +const WORKFLOW_STATE_SCHEMA = createWorkflowStateSchema(STATE_NAMES) + +/** @riviere-role value-object */ +export class WorkflowState { + declare private readonly brand: 'WorkflowState' + + readonly currentStateMachineState: StateName + readonly githubIssue?: number + readonly featureBranch?: string + readonly prNumber?: number + readonly prUrl?: string + readonly architectureReviewPassed: boolean + readonly codeReviewPassed: boolean + readonly bugScannerPassed: boolean + readonly taskCheckPassed: boolean + readonly ciPassed: boolean + readonly feedbackClean: boolean + readonly feedbackAddressed: boolean + readonly feedbackUnresolvedCount?: number + readonly prFeedbackVerificationFailedReason?: string + readonly preBlockedState?: string + readonly transcriptPath?: string + + private constructor(value: z.infer) { + this.currentStateMachineState = value.currentStateMachineState + this.architectureReviewPassed = value.architectureReviewPassed + this.codeReviewPassed = value.codeReviewPassed + this.bugScannerPassed = value.bugScannerPassed + this.taskCheckPassed = value.taskCheckPassed + this.ciPassed = value.ciPassed + this.feedbackClean = value.feedbackClean + this.feedbackAddressed = value.feedbackAddressed + if (value.githubIssue !== undefined) this.githubIssue = value.githubIssue + if (value.featureBranch !== undefined) this.featureBranch = value.featureBranch + if (value.prNumber !== undefined) this.prNumber = value.prNumber + if (value.prUrl !== undefined) this.prUrl = value.prUrl + if (value.feedbackUnresolvedCount !== undefined) { + this.feedbackUnresolvedCount = value.feedbackUnresolvedCount + } + if (value.prFeedbackVerificationFailedReason !== undefined) { + this.prFeedbackVerificationFailedReason = value.prFeedbackVerificationFailedReason + } + if (value.preBlockedState !== undefined) this.preBlockedState = value.preBlockedState + if (value.transcriptPath !== undefined) this.transcriptPath = value.transcriptPath + } + + static parse(value: unknown): WorkflowState { + return new WorkflowState(WORKFLOW_STATE_SCHEMA.parse(value)) + } + + with(changes: Partial>): WorkflowState { + return WorkflowState.parse({ + ...this, + ...changes, + }) + } +} + +const INITIAL_STATE = WorkflowState.parse({ + currentStateMachineState: 'IMPLEMENTING', + architectureReviewPassed: false, + codeReviewPassed: false, + bugScannerPassed: false, + taskCheckPassed: false, + ciPassed: false, + feedbackClean: false, + feedbackAddressed: false, +}) + +/** @riviere-role domain-service */ +export function parseStateName(value: string): StateName { + return STATE_NAME_SCHEMA.parse(value) +} + +/** @riviere-role domain-service */ +export function getWorkflowStateNames() { + return STATE_NAMES +} + +/** @riviere-role domain-service */ +export function getWorkflowStateNameSchema() { + return STATE_NAME_SCHEMA +} + +/** @riviere-role domain-service */ +export function getInitialWorkflowState(): WorkflowState { + return INITIAL_STATE +} diff --git a/tools/dev-workflow-v2/src/features/workflow/domain/workflow.ts b/packages/dev-workflow-v2/domain-model/src/domain/workflow.ts similarity index 91% rename from tools/dev-workflow-v2/src/features/workflow/domain/workflow.ts rename to packages/dev-workflow-v2/domain-model/src/domain/workflow.ts index 2f73d94e0..26666d079 100644 --- a/tools/dev-workflow-v2/src/features/workflow/domain/workflow.ts +++ b/packages/dev-workflow-v2/domain-model/src/domain/workflow.ts @@ -9,25 +9,13 @@ import { defineRecordingOps, checkOperationGate, } from '@nt-ai-lab/deterministic-agent-workflow-dsl' -import type { - BaseEvent, StoredReview -} from '@nt-ai-lab/deterministic-agent-workflow-engine' +import type { BaseEvent, StoredReview } from '@nt-ai-lab/deterministic-agent-workflow-engine' import { WorkflowStateError } from '@nt-ai-lab/deterministic-agent-workflow-engine' -import type { - WorkflowState, - StateName, - WorkflowOperation, - LivingArchitectureReviewType, -} from './workflow-types' -import { - WORKFLOW_REGISTRY, getStateDefinition -} from './registry' -import { WORKFLOW_STATE_SCHEMA } from './workflow-types' +import { getInitialWorkflowState, WorkflowState } from './workflow-types' +import { getStateDefinition, getWorkflowRegistry } from './registry' import type { WorkflowEvent } from './workflow-events' import { parseWorkflowEvent } from './workflow-events' -import { - applyEvent, EMPTY_STATE -} from './fold' +import { applyEvent } from './fold' import { buildPullRequestCreationRequest, parsePullRequestDescriptionOptions, @@ -35,13 +23,29 @@ import { import type { CreateWorkflowPullRequest } from './ports/create-pull-request' import type { ReadWorkflowGitStatus } from './ports/read-git-status' import type { ReadWorkflowPullRequestFeedback } from './ports/read-pull-request-feedback' -import type { WorkflowPullRequestFeedback } from './pull-request-feedback' + +type StateName = WorkflowState['currentStateMachineState'] +type WorkflowOperation = + | 'record-issue' + | 'record-branch' + | 'record-review' + | 'record-pr' + | 'record-ci-passed' + | 'record-ci-failed' + | 'create-pr' + | 'verify-feedback-addressed' +type LivingArchitectureReviewType = + | 'architecture-review' + | 'code-review' + | 'bug-scanner' + | 'task-check' const PR_FEEDBACK_POLL_INTERVAL_MS = 15_000 const PR_FEEDBACK_TIMEOUT_MS = 300_000 const PR_FEEDBACK_MAX_ATTEMPTS = Math.floor(PR_FEEDBACK_TIMEOUT_MS / PR_FEEDBACK_POLL_INTERVAL_MS) + 1 const REQUIRED_CONSECUTIVE_CLEAN_CODERABBIT_POLLS = 2 +const WORKFLOW_REGISTRY = getWorkflowRegistry() const RECORDING_OPS_MAP: Record> = { 'record-issue': { @@ -101,7 +105,7 @@ function diffStateOverrides( return overrides } -function isFeedbackClear(feedback: WorkflowPullRequestFeedback): boolean { +function isFeedbackClear(feedback: ReturnType): boolean { return feedback.reviewDecision !== 'CHANGES_REQUESTED' && feedback.unresolvedCount === 0 } @@ -110,13 +114,13 @@ function readPrFeedback( prNumber: number, ): | { - ok: true - feedback: WorkflowPullRequestFeedback - } + ok: true + feedback: ReturnType + } | { - ok: false - reason: string - } { + ok: false + reason: string + } { try { return { ok: true, @@ -142,11 +146,11 @@ export class Workflow { } static createFresh(deps: WorkflowDeps): Workflow { - return new Workflow(EMPTY_STATE, deps) + return new Workflow(getInitialWorkflowState(), deps) } - static rehydrate(state: WorkflowState, deps: WorkflowDeps): Workflow { - return new Workflow(WORKFLOW_STATE_SCHEMA.parse(state), deps) + static rehydrate(state: unknown, deps: WorkflowDeps): Workflow { + return new Workflow(WorkflowState.parse(state), deps) } getPendingEvents(): readonly WorkflowEvent[] { diff --git a/packages/dev-workflow-v2/domain-model/tsconfig.json b/packages/dev-workflow-v2/domain-model/tsconfig.json new file mode 100644 index 000000000..cf9a15706 --- /dev/null +++ b/packages/dev-workflow-v2/domain-model/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { "path": "./tsconfig.lib.json" }, + { "path": "./tsconfig.spec.json" } + ] +} diff --git a/packages/dev-workflow-v2/domain-model/tsconfig.lib.json b/packages/dev-workflow-v2/domain-model/tsconfig.lib.json new file mode 100644 index 000000000..a36640c99 --- /dev/null +++ b/packages/dev-workflow-v2/domain-model/tsconfig.lib.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": ".", + "rootDir": "src", + "outDir": "dist", + "tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo", + "emitDeclarationOnly": false, + "forceConsistentCasingInFileNames": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.test.ts", "src/**/*.spec.ts", "src/**/__fixtures__/**"] +} diff --git a/packages/dev-workflow-v2/domain-model/tsconfig.spec.json b/packages/dev-workflow-v2/domain-model/tsconfig.spec.json new file mode 100644 index 000000000..dc089a45a --- /dev/null +++ b/packages/dev-workflow-v2/domain-model/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./out-tsc/vitest", + "types": ["vitest/globals", "vitest/importMeta", "vite/client", "node", "vitest"], + "forceConsistentCasingInFileNames": true + }, + "include": ["vitest.config.mts", "src/**/*.spec.ts", "src/**/*.d.ts", "src/**/__fixtures__/**/*.ts"], + "references": [{ "path": "./tsconfig.lib.json" }] +} diff --git a/packages/dev-workflow-v2/domain-model/vitest.config.mts b/packages/dev-workflow-v2/domain-model/vitest.config.mts new file mode 100644 index 000000000..8e94b4bc0 --- /dev/null +++ b/packages/dev-workflow-v2/domain-model/vitest.config.mts @@ -0,0 +1,20 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + globals: true, + coverage: { + enabled: true, + reportsDirectory: './test-output/vitest/coverage', + provider: 'v8', + include: ['src/**/*.ts'], + exclude: ['src/**/*.spec.ts', 'src/**/__fixtures__/**'], + thresholds: { + lines: 100, + statements: 100, + functions: 100, + branches: 100, + }, + }, + }, +}) diff --git a/packages/dev-workflow-v2/use-cases/package.json b/packages/dev-workflow-v2/use-cases/package.json new file mode 100644 index 000000000..04ed74ad4 --- /dev/null +++ b/packages/dev-workflow-v2/use-cases/package.json @@ -0,0 +1,39 @@ +{ + "name": "@living-architecture/dev-workflow-v2-use-cases", + "version": "0.0.1", + "private": true, + "type": "module", + "exports": { + "./package.json": "./package.json", + "./commands/*": { + "@living-architecture/source": "./src/features/workflow/commands/*.ts", + "types": "./dist/features/workflow/commands/*.d.ts", + "import": "./dist/features/workflow/commands/*.js", + "default": "./dist/features/workflow/commands/*.js" + }, + "./adapters/*": { + "@living-architecture/source": "./src/features/workflow/adapters/*.ts", + "types": "./dist/features/workflow/adapters/*.d.ts", + "import": "./dist/features/workflow/adapters/*.js", + "default": "./dist/features/workflow/adapters/*.js" + }, + "./external-clients/*": { + "@living-architecture/source": "./src/infra/external-clients/*.ts", + "types": "./dist/infra/external-clients/*.d.ts", + "import": "./dist/infra/external-clients/*.js", + "default": "./dist/infra/external-clients/*.js" + } + }, + "dependencies": { + "@living-architecture/dev-workflow-v2-domain-model": "workspace:*", + "@nt-ai-lab/deterministic-agent-workflow-dsl": "0.3.6", + "@nt-ai-lab/deterministic-agent-workflow-engine": "0.3.6", + "zod": "^3.23.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@vitest/coverage-v8": "^2.0.0", + "typescript": "^5.6.0", + "vitest": "^2.0.0" + } +} diff --git a/tools/dev-workflow-v2/src/features/workflow/adapters/git/workflow-git-status-reader.spec.ts b/packages/dev-workflow-v2/use-cases/src/features/workflow/adapters/git/workflow-git-status-reader.spec.ts similarity index 100% rename from tools/dev-workflow-v2/src/features/workflow/adapters/git/workflow-git-status-reader.spec.ts rename to packages/dev-workflow-v2/use-cases/src/features/workflow/adapters/git/workflow-git-status-reader.spec.ts diff --git a/packages/dev-workflow-v2/use-cases/src/features/workflow/adapters/git/workflow-git-status-reader.ts b/packages/dev-workflow-v2/use-cases/src/features/workflow/adapters/git/workflow-git-status-reader.ts new file mode 100644 index 000000000..329dd9bba --- /dev/null +++ b/packages/dev-workflow-v2/use-cases/src/features/workflow/adapters/git/workflow-git-status-reader.ts @@ -0,0 +1,18 @@ +import type { ReadWorkflowGitStatus } from '@living-architecture/dev-workflow-v2-domain-model/domain/ports/read-git-status' +import type { GitRepositoryStatus } from '../../../../infra/external-clients/git/git-client' + +/** @riviere-role domain-port-adapter */ +export function createWorkflowGitStatusReader( + readGitRepositoryStatus: () => GitRepositoryStatus, +): ReadWorkflowGitStatus { + return () => { + const status = readGitRepositoryStatus() + return { + changedFilesVsDefault: status.changedFilesVsDefault, + currentBranch: status.currentBranch, + hasCommitsVsDefault: status.hasCommitsVsDefault, + headCommit: status.headCommit, + workingTreeClean: status.workingTreeClean, + } + } +} diff --git a/tools/dev-workflow-v2/src/features/workflow/adapters/github/workflow-pull-request-creator.spec.ts b/packages/dev-workflow-v2/use-cases/src/features/workflow/adapters/github/workflow-pull-request-creator.spec.ts similarity index 100% rename from tools/dev-workflow-v2/src/features/workflow/adapters/github/workflow-pull-request-creator.spec.ts rename to packages/dev-workflow-v2/use-cases/src/features/workflow/adapters/github/workflow-pull-request-creator.spec.ts diff --git a/packages/dev-workflow-v2/use-cases/src/features/workflow/adapters/github/workflow-pull-request-creator.ts b/packages/dev-workflow-v2/use-cases/src/features/workflow/adapters/github/workflow-pull-request-creator.ts new file mode 100644 index 000000000..533b5f4f4 --- /dev/null +++ b/packages/dev-workflow-v2/use-cases/src/features/workflow/adapters/github/workflow-pull-request-creator.ts @@ -0,0 +1,22 @@ +import type { + GithubPullRequest, + GithubPullRequestCreationInput, +} from '../../../../infra/external-clients/github/create-pull-request' +import type { CreateWorkflowPullRequest } from '@living-architecture/dev-workflow-v2-domain-model/domain/ports/create-pull-request' + +/** @riviere-role domain-port-adapter */ +export function createWorkflowPullRequestCreator( + createGithubPullRequest: (input: GithubPullRequestCreationInput) => GithubPullRequest, +): CreateWorkflowPullRequest { + return (request) => { + const pullRequest = createGithubPullRequest({ + body: request.body, + title: request.title, + }) + return { + isDraft: pullRequest.isDraft, + prNumber: pullRequest.prNumber, + prUrl: pullRequest.prUrl, + } + } +} diff --git a/tools/dev-workflow-v2/src/features/workflow/adapters/github/workflow-pull-request-feedback-reader.spec.ts b/packages/dev-workflow-v2/use-cases/src/features/workflow/adapters/github/workflow-pull-request-feedback-reader.spec.ts similarity index 100% rename from tools/dev-workflow-v2/src/features/workflow/adapters/github/workflow-pull-request-feedback-reader.spec.ts rename to packages/dev-workflow-v2/use-cases/src/features/workflow/adapters/github/workflow-pull-request-feedback-reader.spec.ts diff --git a/tools/dev-workflow-v2/src/features/workflow/adapters/github/workflow-pull-request-feedback-reader.ts b/packages/dev-workflow-v2/use-cases/src/features/workflow/adapters/github/workflow-pull-request-feedback-reader.ts similarity index 78% rename from tools/dev-workflow-v2/src/features/workflow/adapters/github/workflow-pull-request-feedback-reader.ts rename to packages/dev-workflow-v2/use-cases/src/features/workflow/adapters/github/workflow-pull-request-feedback-reader.ts index 0d78d707d..ab0902e72 100644 --- a/tools/dev-workflow-v2/src/features/workflow/adapters/github/workflow-pull-request-feedback-reader.ts +++ b/packages/dev-workflow-v2/use-cases/src/features/workflow/adapters/github/workflow-pull-request-feedback-reader.ts @@ -1,5 +1,5 @@ -import type { GithubPullRequestFeedback } from '../../../../platform/infra/external-clients/github/index' -import type { ReadWorkflowPullRequestFeedback } from '../../domain/ports/read-pull-request-feedback' +import type { ReadWorkflowPullRequestFeedback } from '@living-architecture/dev-workflow-v2-domain-model/domain/ports/read-pull-request-feedback' +import type { GithubPullRequestFeedback } from '../../../../infra/external-clients/github/get-pr-feedback' /** @riviere-role domain-port-adapter */ export function createWorkflowPullRequestFeedbackReader( diff --git a/packages/dev-workflow-v2/use-cases/src/features/workflow/commands/configure-workflow.spec.ts b/packages/dev-workflow-v2/use-cases/src/features/workflow/commands/configure-workflow.spec.ts new file mode 100644 index 000000000..d5df857bd --- /dev/null +++ b/packages/dev-workflow-v2/use-cases/src/features/workflow/commands/configure-workflow.spec.ts @@ -0,0 +1,229 @@ +import { configureWorkflow } from './configure-workflow' +import { Workflow } from '@living-architecture/dev-workflow-v2-domain-model/domain/workflow' +import type { BaseEvent } from '@nt-ai-lab/deterministic-agent-workflow-engine' +import { WorkflowStateError } from '@nt-ai-lab/deterministic-agent-workflow-engine' +import { WorkflowState } from '@living-architecture/dev-workflow-v2-domain-model/domain/workflow-types' + +type WorkflowDeps = Parameters[1] +type StateName = WorkflowState['currentStateMachineState'] +const WORKFLOW_DEFINITION = configureWorkflow({}) + +function makeWorkflowDeps(): WorkflowDeps { + return { + getGitInfo: () => ({ + currentBranch: 'main', + workingTreeClean: true, + headCommit: 'abc123', + changedFilesVsDefault: [], + hasCommitsVsDefault: false, + }), + getPrFeedback: () => ({ + reviewDecision: null, + coderabbitReviewSeen: true, + unresolvedCount: 0, + threads: [], + }), + createPullRequest: () => ({ + prNumber: 1, + prUrl: 'https://github.com/example/repo/pull/1', + isDraft: false, + }), + listSessionReviews: () => [], + sleepMs: () => undefined, + now: () => '2026-01-01T00:00:00Z', + } +} + +function buildTransitionEvent( + from: StateName, + to: StateName, + stateBefore: WorkflowState, + stateAfter: WorkflowState, + now: string, +): BaseEvent { + const fn = WORKFLOW_DEFINITION.buildTransitionEvent + if (fn === undefined) throw new WorkflowStateError('buildTransitionEvent not defined') + return fn(from, to, stateBefore, stateAfter, now) +} + +describe('WORKFLOW_DEFINITION', () => { + it('builds a Workflow in IMPLEMENTING state from initial state', () => { + const workflow = WORKFLOW_DEFINITION.buildWorkflow( + WORKFLOW_DEFINITION.initialState(), + makeWorkflowDeps(), + ) + expect(workflow.getState().currentStateMachineState).toStrictEqual('IMPLEMENTING') + }) + + it('builds a Workflow from initial state (pass-through, no events folded)', () => { + const state = WORKFLOW_DEFINITION.initialState() + const workflow = WORKFLOW_DEFINITION.buildWorkflow(state, makeWorkflowDeps()) + expect(workflow.getState().currentStateMachineState).toStrictEqual('IMPLEMENTING') + }) + + it('folds a valid event onto state', () => { + const event: BaseEvent & Record = { + type: 'issue-recorded', + at: '2026-01-01T00:00:00Z', + issueNumber: 42, + } + const state = WORKFLOW_DEFINITION.fold(WORKFLOW_DEFINITION.initialState(), event) + const workflow = WORKFLOW_DEFINITION.buildWorkflow(state, makeWorkflowDeps()) + expect(workflow.getState().githubIssue).toStrictEqual(42) + }) + + it('folds session-started event and makes transcriptPath available', () => { + const event: BaseEvent & Record = { + type: 'session-started', + at: '2026-01-01T00:00:00Z', + transcriptPath: 'some/transcript.jsonl', + } + const state = WORKFLOW_DEFINITION.fold(WORKFLOW_DEFINITION.initialState(), event) + const workflow = WORKFLOW_DEFINITION.buildWorkflow(state, makeWorkflowDeps()) + expect(workflow.getTranscriptPath()).toBe('some/transcript.jsonl') + }) + + it('returns state unchanged for unknown event types (e.g. platform observation events)', () => { + const event: BaseEvent = { + type: 'identity-verified', + at: '2026-01-01T00:00:00Z', + } + const state = WORKFLOW_DEFINITION.initialState() + const result = WORKFLOW_DEFINITION.fold(state, event) + expect(result).toStrictEqual(state) + }) + + it('throws when a known event type has a malformed payload', () => { + const malformed: BaseEvent & Record = { + type: 'issue-recorded', + at: '2026-01-01T00:00:00Z', + issueNumber: 'not-a-number', + } + expect(() => WORKFLOW_DEFINITION.fold(WORKFLOW_DEFINITION.initialState(), malformed)).toThrow( + 'Malformed workflow event "issue-recorded"', + ) + }) + + it('returns initial state with IMPLEMENTING', () => { + const initial = WORKFLOW_DEFINITION.initialState() + expect(initial.currentStateMachineState).toStrictEqual('IMPLEMENTING') + }) + + it('stateSchema parses valid state name', () => { + expect(WORKFLOW_DEFINITION.stateSchema.parse('IMPLEMENTING')).toStrictEqual('IMPLEMENTING') + }) + + it('stateSchema throws on invalid state name', () => { + expect(() => WORKFLOW_DEFINITION.stateSchema.parse('UNKNOWN_STATE')).toThrow( + 'Invalid enum value', + ) + }) + + describe('getRegistry', () => { + it('returns the workflow registry', () => { + const registry = WORKFLOW_DEFINITION.getRegistry() + expect(registry.IMPLEMENTING).toBeDefined() + expect(registry.REVIEWING).toBeDefined() + expect(registry.COMPLETE).toBeDefined() + }) + + it('marks COMPLETE and BLOCKED as write-forbidden states', () => { + const registry = WORKFLOW_DEFINITION.getRegistry() + expect(registry.BLOCKED.forbidden).toStrictEqual({ write: true }) + expect(registry.COMPLETE.forbidden).toStrictEqual({ write: true }) + }) + }) + + describe('buildTransitionContext', () => { + it('builds context with state and transition info', () => { + const state = WorkflowState.parse({ + currentStateMachineState: 'IMPLEMENTING', + architectureReviewPassed: false, + codeReviewPassed: false, + bugScannerPassed: false, + taskCheckPassed: false, + ciPassed: false, + feedbackClean: false, + feedbackAddressed: false, + prNumber: 42, + }) + const deps = makeWorkflowDeps() + const ctx = WORKFLOW_DEFINITION.buildTransitionContext( + state, + 'IMPLEMENTING', + 'REVIEWING', + deps, + ) + expect(ctx.state).toBe(state) + expect(ctx.from).toStrictEqual('IMPLEMENTING') + expect(ctx.to).toStrictEqual('REVIEWING') + }) + }) + + describe('buildTransitionEvent', () => { + const baseBefore = WorkflowState.parse({ + currentStateMachineState: 'IMPLEMENTING', + architectureReviewPassed: true, + codeReviewPassed: true, + bugScannerPassed: true, + taskCheckPassed: false, + ciPassed: true, + feedbackClean: true, + feedbackAddressed: true, + }) + + it('produces event without stateOverrides when no state changes', () => { + const event = buildTransitionEvent( + 'IMPLEMENTING', + 'REVIEWING', + baseBefore, + baseBefore, + '2026-01-01T00:00:00Z', + ) + expect(event).toStrictEqual({ + type: 'transitioned', + at: '2026-01-01T00:00:00Z', + from: 'IMPLEMENTING', + to: 'REVIEWING', + }) + }) + + it('produces event with stateOverrides when onEntry mutates state', () => { + const stateAfter = baseBefore.with({ + architectureReviewPassed: false, + codeReviewPassed: false, + bugScannerPassed: false, + ciPassed: false, + feedbackClean: false, + feedbackAddressed: false, + }) + const event = buildTransitionEvent( + 'REVIEWING', + 'IMPLEMENTING', + baseBefore, + stateAfter, + '2026-01-01T00:00:00Z', + ) + expect(event).toHaveProperty('stateOverrides', { + architectureReviewPassed: false, + codeReviewPassed: false, + bugScannerPassed: false, + ciPassed: false, + feedbackClean: false, + feedbackAddressed: false, + }) + }) + + it('does not include currentStateMachineState in stateOverrides', () => { + const stateAfter = baseBefore.with({ currentStateMachineState: 'REVIEWING' }) + const event = buildTransitionEvent( + 'IMPLEMENTING', + 'REVIEWING', + baseBefore, + stateAfter, + '2026-01-01T00:00:00Z', + ) + expect(event).not.toHaveProperty('stateOverrides') + }) + }) +}) diff --git a/packages/dev-workflow-v2/use-cases/src/features/workflow/commands/configure-workflow.ts b/packages/dev-workflow-v2/use-cases/src/features/workflow/commands/configure-workflow.ts new file mode 100644 index 000000000..1747c3a63 --- /dev/null +++ b/packages/dev-workflow-v2/use-cases/src/features/workflow/commands/configure-workflow.ts @@ -0,0 +1,101 @@ +import type { BaseEvent, WorkflowDefinition } from '@nt-ai-lab/deterministic-agent-workflow-engine' +import { WorkflowStateError } from '@nt-ai-lab/deterministic-agent-workflow-engine' +import type { TransitionContext } from '@nt-ai-lab/deterministic-agent-workflow-dsl' +import { applyEvent } from '@living-architecture/dev-workflow-v2-domain-model/domain/fold' +import { + getOperationBody, + getTransitionTitle, +} from '@living-architecture/dev-workflow-v2-domain-model/domain/output-messages' +import { getWorkflowRegistry } from '@living-architecture/dev-workflow-v2-domain-model/domain/registry' +import { Workflow } from '@living-architecture/dev-workflow-v2-domain-model/domain/workflow' +import { + getKnownWorkflowEventTypes, + parseWorkflowEvent, +} from '@living-architecture/dev-workflow-v2-domain-model/domain/workflow-events' +import type { WorkflowState } from '@living-architecture/dev-workflow-v2-domain-model/domain/workflow-types' +import { + getInitialWorkflowState, + getWorkflowStateNameSchema, +} from '@living-architecture/dev-workflow-v2-domain-model/domain/workflow-types' +import { isWriteAllowed } from '@living-architecture/dev-workflow-v2-domain-model/domain/workflow-predicates' + +type WorkflowDeps = Parameters[1] +type StateName = WorkflowState['currentStateMachineState'] +type WorkflowOperation = Parameters[0] +/** @riviere-role command-use-case-result */ +export type ConfigureWorkflowResult = WorkflowDefinition< + Workflow, + WorkflowState, + WorkflowDeps, + StateName, + WorkflowOperation +> & { + readonly isWriteAllowed: typeof isWriteAllowed +} +const KNOWN_EVENT_TYPES: ReadonlySet = new Set(getKnownWorkflowEventTypes()) + +/** @riviere-role command-use-case-input */ +export type ConfigureWorkflowInput = Readonly> + +function diffStateOverrides( + stateBefore: WorkflowState, + stateAfter: WorkflowState, +): Record { + const overrides: Record = {} + const beforeEntries = new Map(Object.entries(stateBefore)) + for (const [key, value] of Object.entries(stateAfter)) { + if (key === 'currentStateMachineState') continue + if (value !== beforeEntries.get(key)) overrides[key] = value + } + return overrides +} + +/** @riviere-role command-use-case */ +export function configureWorkflow(input: ConfigureWorkflowInput): ConfigureWorkflowResult { + void input + return { + fold(state: WorkflowState, event: BaseEvent): WorkflowState { + try { + return applyEvent(state, parseWorkflowEvent(event)) + } catch (error) { + if (KNOWN_EVENT_TYPES.has(event.type)) { + throw new WorkflowStateError(`Malformed workflow event "${event.type}": ${String(error)}`) + } + return state + } + }, + buildWorkflow(state: WorkflowState, deps: WorkflowDeps): Workflow { + return Workflow.rehydrate(state, deps) + }, + stateSchema: getWorkflowStateNameSchema(), + initialState: getInitialWorkflowState, + getRegistry: getWorkflowRegistry, + buildTransitionContext( + state: WorkflowState, + from: StateName, + to: StateName, + deps: WorkflowDeps, + ): TransitionContext { + return { state, gitInfo: deps.getGitInfo(), from, to } + }, + buildTransitionEvent( + from: StateName, + to: StateName, + stateBefore: WorkflowState, + stateAfter: WorkflowState, + now: string, + ): BaseEvent { + const overrides = diffStateOverrides(stateBefore, stateAfter) + return { + type: 'transitioned', + at: now, + from, + to, + ...(Object.keys(overrides).length > 0 ? { stateOverrides: overrides } : {}), + } + }, + getOperationBody, + getTransitionTitle, + isWriteAllowed, + } +} diff --git a/packages/dev-workflow-v2/use-cases/src/features/workflow/commands/create-pull-request.ts b/packages/dev-workflow-v2/use-cases/src/features/workflow/commands/create-pull-request.ts new file mode 100644 index 000000000..fad09373c --- /dev/null +++ b/packages/dev-workflow-v2/use-cases/src/features/workflow/commands/create-pull-request.ts @@ -0,0 +1,16 @@ +import type { Workflow } from '@living-architecture/dev-workflow-v2-domain-model/domain/workflow' +import type { WorkflowCommandResult } from './workflow-command-result' + +/** @riviere-role command-use-case-input */ +export interface CreatePullRequestInput { + readonly arguments: readonly string[] +} + +/** @riviere-role command-use-case */ +export class CreatePullRequest { + constructor(private readonly workflow: Workflow) {} + + execute(input: CreatePullRequestInput): WorkflowCommandResult { + return this.workflow.createPr(input.arguments) + } +} diff --git a/packages/dev-workflow-v2/use-cases/src/features/workflow/commands/record-branch.ts b/packages/dev-workflow-v2/use-cases/src/features/workflow/commands/record-branch.ts new file mode 100644 index 000000000..eb2247abd --- /dev/null +++ b/packages/dev-workflow-v2/use-cases/src/features/workflow/commands/record-branch.ts @@ -0,0 +1,16 @@ +import type { Workflow } from '@living-architecture/dev-workflow-v2-domain-model/domain/workflow' +import type { WorkflowCommandResult } from './workflow-command-result' + +/** @riviere-role command-use-case-input */ +export interface RecordBranchInput { + readonly branch: string +} + +/** @riviere-role command-use-case */ +export class RecordBranch { + constructor(private readonly workflow: Workflow) {} + + execute(input: RecordBranchInput): WorkflowCommandResult { + return this.workflow.executeRecording('record-branch', input.branch) + } +} diff --git a/packages/dev-workflow-v2/use-cases/src/features/workflow/commands/record-ci-failed.ts b/packages/dev-workflow-v2/use-cases/src/features/workflow/commands/record-ci-failed.ts new file mode 100644 index 000000000..f16f97107 --- /dev/null +++ b/packages/dev-workflow-v2/use-cases/src/features/workflow/commands/record-ci-failed.ts @@ -0,0 +1,16 @@ +import type { Workflow } from '@living-architecture/dev-workflow-v2-domain-model/domain/workflow' +import type { WorkflowCommandResult } from './workflow-command-result' + +/** @riviere-role command-use-case-input */ +export interface RecordCiFailedInput { + readonly output: string +} + +/** @riviere-role command-use-case */ +export class RecordCiFailed { + constructor(private readonly workflow: Workflow) {} + + execute(input: RecordCiFailedInput): WorkflowCommandResult { + return this.workflow.executeRecording('record-ci-failed', input.output) + } +} diff --git a/packages/dev-workflow-v2/use-cases/src/features/workflow/commands/record-ci-passed.ts b/packages/dev-workflow-v2/use-cases/src/features/workflow/commands/record-ci-passed.ts new file mode 100644 index 000000000..aa920e92a --- /dev/null +++ b/packages/dev-workflow-v2/use-cases/src/features/workflow/commands/record-ci-passed.ts @@ -0,0 +1,15 @@ +import type { Workflow } from '@living-architecture/dev-workflow-v2-domain-model/domain/workflow' +import type { WorkflowCommandResult } from './workflow-command-result' + +/** @riviere-role command-use-case-input */ +export type RecordCiPassedInput = Record + +/** @riviere-role command-use-case */ +export class RecordCiPassed { + constructor(private readonly workflow: Workflow) {} + + execute(input: RecordCiPassedInput): WorkflowCommandResult { + void input + return this.workflow.executeRecording('record-ci-passed') + } +} diff --git a/packages/dev-workflow-v2/use-cases/src/features/workflow/commands/record-issue.ts b/packages/dev-workflow-v2/use-cases/src/features/workflow/commands/record-issue.ts new file mode 100644 index 000000000..2208ccfe3 --- /dev/null +++ b/packages/dev-workflow-v2/use-cases/src/features/workflow/commands/record-issue.ts @@ -0,0 +1,16 @@ +import type { Workflow } from '@living-architecture/dev-workflow-v2-domain-model/domain/workflow' +import type { WorkflowCommandResult } from './workflow-command-result' + +/** @riviere-role command-use-case-input */ +export interface RecordIssueInput { + readonly issueNumber: number +} + +/** @riviere-role command-use-case */ +export class RecordIssue { + constructor(private readonly workflow: Workflow) {} + + execute(input: RecordIssueInput): WorkflowCommandResult { + return this.workflow.executeRecording('record-issue', input.issueNumber) + } +} diff --git a/packages/dev-workflow-v2/use-cases/src/features/workflow/commands/record-pull-request.ts b/packages/dev-workflow-v2/use-cases/src/features/workflow/commands/record-pull-request.ts new file mode 100644 index 000000000..da7bc5bca --- /dev/null +++ b/packages/dev-workflow-v2/use-cases/src/features/workflow/commands/record-pull-request.ts @@ -0,0 +1,17 @@ +import type { Workflow } from '@living-architecture/dev-workflow-v2-domain-model/domain/workflow' +import type { WorkflowCommandResult } from './workflow-command-result' + +/** @riviere-role command-use-case-input */ +export interface RecordPullRequestInput { + readonly number: number + readonly url: string | undefined +} + +/** @riviere-role command-use-case */ +export class RecordPullRequest { + constructor(private readonly workflow: Workflow) {} + + execute(input: RecordPullRequestInput): WorkflowCommandResult { + return this.workflow.executeRecording('record-pr', input.number, input.url) + } +} diff --git a/packages/dev-workflow-v2/use-cases/src/features/workflow/commands/verify-feedback-addressed.ts b/packages/dev-workflow-v2/use-cases/src/features/workflow/commands/verify-feedback-addressed.ts new file mode 100644 index 000000000..5bd54d52a --- /dev/null +++ b/packages/dev-workflow-v2/use-cases/src/features/workflow/commands/verify-feedback-addressed.ts @@ -0,0 +1,15 @@ +import type { Workflow } from '@living-architecture/dev-workflow-v2-domain-model/domain/workflow' +import type { WorkflowCommandResult } from './workflow-command-result' + +/** @riviere-role command-use-case-input */ +export type VerifyFeedbackAddressedInput = Record + +/** @riviere-role command-use-case */ +export class VerifyFeedbackAddressed { + constructor(private readonly workflow: Workflow) {} + + execute(input: VerifyFeedbackAddressedInput): WorkflowCommandResult { + void input + return this.workflow.verifyFeedbackAddressed() + } +} diff --git a/packages/dev-workflow-v2/use-cases/src/features/workflow/commands/workflow-command-result.ts b/packages/dev-workflow-v2/use-cases/src/features/workflow/commands/workflow-command-result.ts new file mode 100644 index 000000000..108a40c2c --- /dev/null +++ b/packages/dev-workflow-v2/use-cases/src/features/workflow/commands/workflow-command-result.ts @@ -0,0 +1,4 @@ +/** @riviere-role command-use-case-result */ +export type WorkflowCommandResult = + | { readonly pass: true } + | { readonly pass: false; readonly reason: string } diff --git a/packages/dev-workflow-v2/use-cases/src/features/workflow/commands/workflow-commands.spec.ts b/packages/dev-workflow-v2/use-cases/src/features/workflow/commands/workflow-commands.spec.ts new file mode 100644 index 000000000..6a72da40e --- /dev/null +++ b/packages/dev-workflow-v2/use-cases/src/features/workflow/commands/workflow-commands.spec.ts @@ -0,0 +1,73 @@ +import { Workflow } from '@living-architecture/dev-workflow-v2-domain-model/domain/workflow' +import { getInitialWorkflowState } from '@living-architecture/dev-workflow-v2-domain-model/domain/workflow-types' +import { CreatePullRequest } from './create-pull-request' +import { RecordBranch } from './record-branch' +import { RecordCiFailed } from './record-ci-failed' +import { RecordCiPassed } from './record-ci-passed' +import { RecordIssue } from './record-issue' +import { RecordPullRequest } from './record-pull-request' +import { VerifyFeedbackAddressed } from './verify-feedback-addressed' + +type WorkflowDeps = Parameters[1] + +function workflow(): Workflow { + const deps: WorkflowDeps = { + getGitInfo: () => ({ + currentBranch: 'feature/test', + workingTreeClean: true, + headCommit: 'abc123', + changedFilesVsDefault: [], + hasCommitsVsDefault: true, + }), + getPrFeedback: () => ({ + reviewDecision: null, + coderabbitReviewSeen: true, + unresolvedCount: 0, + threads: [], + }), + createPullRequest: () => ({ + prNumber: 42, + prUrl: 'https://github.com/example/repo/pull/42', + isDraft: false, + }), + listSessionReviews: () => [], + sleepMs: () => undefined, + now: () => '2026-01-01T00:00:00Z', + } + return Workflow.rehydrate(getInitialWorkflowState(), deps) +} + +describe('workflow commands', () => { + it('creates a pull request', () => { + expect(new CreatePullRequest(workflow()).execute({ arguments: [] })).toHaveProperty('pass') + }) + + it('records a branch', () => { + expect(new RecordBranch(workflow()).execute({ branch: 'feature/test' })).toHaveProperty('pass') + }) + + it('records failed CI', () => { + expect(new RecordCiFailed(workflow()).execute({ output: 'failed' })).toHaveProperty('pass') + }) + + it('records passed CI', () => { + expect(new RecordCiPassed(workflow()).execute({})).toHaveProperty('pass') + }) + + it('records an issue', () => { + expect(new RecordIssue(workflow()).execute({ issueNumber: 42 })).toHaveProperty('pass') + }) + + it('records a pull request', () => { + expect( + new RecordPullRequest(workflow()).execute({ + number: 42, + url: 'https://github.com/example/repo/pull/42', + }), + ).toHaveProperty('pass') + }) + + it('verifies addressed feedback', () => { + expect(new VerifyFeedbackAddressed(workflow()).execute({})).toHaveProperty('pass') + }) +}) diff --git a/tools/dev-workflow-v2/src/platform/infra/external-clients/git/git-client.spec.ts b/packages/dev-workflow-v2/use-cases/src/infra/external-clients/git/git-client.spec.ts similarity index 97% rename from tools/dev-workflow-v2/src/platform/infra/external-clients/git/git-client.spec.ts rename to packages/dev-workflow-v2/use-cases/src/infra/external-clients/git/git-client.spec.ts index cd4bfc85a..531f42979 100644 --- a/tools/dev-workflow-v2/src/platform/infra/external-clients/git/git-client.spec.ts +++ b/packages/dev-workflow-v2/use-cases/src/infra/external-clients/git/git-client.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import { readGitRepositoryStatus } from './index' +import { readGitRepositoryStatus } from './git-client' describe('readGitRepositoryStatus', () => { it('reads repository status using the remote default branch', () => { diff --git a/tools/dev-workflow-v2/src/platform/infra/external-clients/git/git-client.ts b/packages/dev-workflow-v2/use-cases/src/infra/external-clients/git/git-client.ts similarity index 100% rename from tools/dev-workflow-v2/src/platform/infra/external-clients/git/git-client.ts rename to packages/dev-workflow-v2/use-cases/src/infra/external-clients/git/git-client.ts diff --git a/tools/dev-workflow-v2/src/platform/infra/external-clients/github/create-pull-request.spec.ts b/packages/dev-workflow-v2/use-cases/src/infra/external-clients/github/create-pull-request.spec.ts similarity index 96% rename from tools/dev-workflow-v2/src/platform/infra/external-clients/github/create-pull-request.spec.ts rename to packages/dev-workflow-v2/use-cases/src/infra/external-clients/github/create-pull-request.spec.ts index 1fd0101f7..6b91a0d87 100644 --- a/tools/dev-workflow-v2/src/platform/infra/external-clients/github/create-pull-request.spec.ts +++ b/packages/dev-workflow-v2/use-cases/src/infra/external-clients/github/create-pull-request.spec.ts @@ -1,7 +1,5 @@ -import { - describe, expect, it -} from 'vitest' -import { createGithubPullRequestClient } from './index' +import { describe, expect, it } from 'vitest' +import { createGithubPullRequestClient } from './create-pull-request' describe('createGithubPullRequestClient', () => { it('creates pull request from structured title and body', () => { diff --git a/tools/dev-workflow-v2/src/platform/infra/external-clients/github/create-pull-request.ts b/packages/dev-workflow-v2/use-cases/src/infra/external-clients/github/create-pull-request.ts similarity index 100% rename from tools/dev-workflow-v2/src/platform/infra/external-clients/github/create-pull-request.ts rename to packages/dev-workflow-v2/use-cases/src/infra/external-clients/github/create-pull-request.ts diff --git a/tools/dev-workflow-v2/src/platform/infra/external-clients/github/get-pr-feedback.spec.ts b/packages/dev-workflow-v2/use-cases/src/infra/external-clients/github/get-pr-feedback.spec.ts similarity index 98% rename from tools/dev-workflow-v2/src/platform/infra/external-clients/github/get-pr-feedback.spec.ts rename to packages/dev-workflow-v2/use-cases/src/infra/external-clients/github/get-pr-feedback.spec.ts index 3a61b6b29..a1886acf2 100644 --- a/tools/dev-workflow-v2/src/platform/infra/external-clients/github/get-pr-feedback.spec.ts +++ b/packages/dev-workflow-v2/use-cases/src/infra/external-clients/github/get-pr-feedback.spec.ts @@ -1,7 +1,5 @@ -import { - describe, it, expect, vi -} from 'vitest' -import { createGithubPullRequestFeedbackClient } from './index' +import { describe, it, expect, vi } from 'vitest' +import { createGithubPullRequestFeedbackClient } from './get-pr-feedback' const REPO_INFO = JSON.stringify({ owner: { login: 'TestOwner' }, diff --git a/tools/dev-workflow-v2/src/platform/infra/external-clients/github/get-pr-feedback.ts b/packages/dev-workflow-v2/use-cases/src/infra/external-clients/github/get-pr-feedback.ts similarity index 100% rename from tools/dev-workflow-v2/src/platform/infra/external-clients/github/get-pr-feedback.ts rename to packages/dev-workflow-v2/use-cases/src/infra/external-clients/github/get-pr-feedback.ts diff --git a/tools/dev-workflow-v2/src/platform/infra/external-clients/github/github-cli.spec.ts b/packages/dev-workflow-v2/use-cases/src/infra/external-clients/github/github-cli.spec.ts similarity index 90% rename from tools/dev-workflow-v2/src/platform/infra/external-clients/github/github-cli.spec.ts rename to packages/dev-workflow-v2/use-cases/src/infra/external-clients/github/github-cli.spec.ts index 0ab9a57b1..9d64eb730 100644 --- a/tools/dev-workflow-v2/src/platform/infra/external-clients/github/github-cli.spec.ts +++ b/packages/dev-workflow-v2/use-cases/src/infra/external-clients/github/github-cli.spec.ts @@ -1,5 +1,5 @@ import { expect, it, vi } from 'vitest' -import { runGh } from './index' +import { runGh } from './github-cli' it('executes GitHub CLI arguments without interpreting them in a shell', () => { const executeGithub = vi.fn(() => 'result') diff --git a/tools/dev-workflow-v2/src/platform/infra/external-clients/github/github-cli.ts b/packages/dev-workflow-v2/use-cases/src/infra/external-clients/github/github-cli.ts similarity index 100% rename from tools/dev-workflow-v2/src/platform/infra/external-clients/github/github-cli.ts rename to packages/dev-workflow-v2/use-cases/src/infra/external-clients/github/github-cli.ts diff --git a/packages/dev-workflow-v2/use-cases/tsconfig.json b/packages/dev-workflow-v2/use-cases/tsconfig.json new file mode 100644 index 000000000..cf9a15706 --- /dev/null +++ b/packages/dev-workflow-v2/use-cases/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { "path": "./tsconfig.lib.json" }, + { "path": "./tsconfig.spec.json" } + ] +} diff --git a/packages/dev-workflow-v2/use-cases/tsconfig.lib.json b/packages/dev-workflow-v2/use-cases/tsconfig.lib.json new file mode 100644 index 000000000..b42efb48b --- /dev/null +++ b/packages/dev-workflow-v2/use-cases/tsconfig.lib.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": ".", + "rootDir": "src", + "outDir": "dist", + "tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo", + "emitDeclarationOnly": false, + "forceConsistentCasingInFileNames": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "references": [{ "path": "../domain-model/tsconfig.lib.json" }], + "exclude": ["src/**/*.test.ts", "src/**/*.spec.ts", "src/**/fixtures/**"] +} diff --git a/packages/dev-workflow-v2/use-cases/tsconfig.spec.json b/packages/dev-workflow-v2/use-cases/tsconfig.spec.json new file mode 100644 index 000000000..e815819b2 --- /dev/null +++ b/packages/dev-workflow-v2/use-cases/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./out-tsc/vitest", + "types": ["vitest/globals", "vitest/importMeta", "vite/client", "node", "vitest"], + "forceConsistentCasingInFileNames": true + }, + "include": ["vitest.config.mts", "src/**/*.spec.ts", "src/**/*.d.ts", "src/**/fixtures/**/*.ts"], + "references": [{ "path": "./tsconfig.lib.json" }] +} diff --git a/packages/dev-workflow-v2/use-cases/vitest.config.mts b/packages/dev-workflow-v2/use-cases/vitest.config.mts new file mode 100644 index 000000000..36faf761d --- /dev/null +++ b/packages/dev-workflow-v2/use-cases/vitest.config.mts @@ -0,0 +1,20 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + globals: true, + coverage: { + enabled: true, + reportsDirectory: './test-output/vitest/coverage', + provider: 'v8', + include: ['src/**/*.ts'], + exclude: ['src/**/*.spec.ts', 'src/**/*-test-fixtures.ts'], + thresholds: { + lines: 100, + statements: 100, + functions: 100, + branches: 100, + }, + }, + }, +}) diff --git a/packages/riviere-builder/README.md b/packages/riviere-builder/README.md deleted file mode 100644 index ea9fa5d08..000000000 --- a/packages/riviere-builder/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# @living-architecture/riviere-builder - -Construct Riviere architecture graphs programmatically. - -## Install - -```bash -npm install @living-architecture/riviere-builder -``` - -## Documentation - -See [apps/docs](../../apps/docs) for full documentation. diff --git a/packages/riviere-builder/CLAUDE.md b/packages/riviere-builder/domain-model/CLAUDE.md similarity index 100% rename from packages/riviere-builder/CLAUDE.md rename to packages/riviere-builder/domain-model/CLAUDE.md diff --git a/packages/riviere-builder/domain-model/README.md b/packages/riviere-builder/domain-model/README.md new file mode 100644 index 000000000..6c16969e2 --- /dev/null +++ b/packages/riviere-builder/domain-model/README.md @@ -0,0 +1,13 @@ +# @living-architecture/riviere-builder-domain-model + +Construct and query Riviere architecture graphs programmatically. + +## Install + +```bash +npm install @living-architecture/riviere-builder-domain-model +``` + +## Documentation + +See [apps/docs](../../apps/docs) for full documentation. diff --git a/packages/riviere-builder/domain-model/package.json b/packages/riviere-builder/domain-model/package.json new file mode 100644 index 000000000..8a31e25ff --- /dev/null +++ b/packages/riviere-builder/domain-model/package.json @@ -0,0 +1,52 @@ +{ + "name": "@living-architecture/riviere-builder-domain-model", + "version": "0.10.3", + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/NTCoding/living-architecture.git", + "directory": "packages/riviere-builder/domain-model" + }, + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "@living-architecture/source": "./src/index.ts", + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./query": { + "@living-architecture/source": "./src/domain/query/RiviereQuery.ts", + "types": "./dist/domain/query/RiviereQuery.d.ts", + "import": "./dist/domain/query/RiviereQuery.js", + "default": "./dist/domain/query/RiviereQuery.js" + }, + "./query/*": { + "@living-architecture/source": "./src/domain/query/*.ts", + "types": "./dist/domain/query/*.d.ts", + "import": "./dist/domain/query/*.js", + "default": "./dist/domain/query/*.js" + }, + "./domain/*": { + "@living-architecture/source": "./src/domain/*.ts", + "types": "./dist/domain/*.d.ts", + "import": "./dist/domain/*.js", + "default": "./dist/domain/*.js" + } + }, + "files": [ + "dist", + "!dist/**/__fixtures__/**", + "!**/*.tsbuildinfo" + ], + "dependencies": { + "@living-architecture/riviere-schema-published-language": "workspace:*", + "zod": "^4.2.1" + } +} diff --git a/packages/riviere-builder/domain-model/project.json b/packages/riviere-builder/domain-model/project.json new file mode 100644 index 000000000..efda4da6c --- /dev/null +++ b/packages/riviere-builder/domain-model/project.json @@ -0,0 +1,26 @@ +{ + "name": "riviere-builder-domain-model", + "targets": { + "build": { + "executor": "nx:run-commands", + "options": { + "commands": [ + "node ../../../scripts/build-public-exports.mjs . --clean", + "tsc --build tsconfig.lib.json", + "node ../../../scripts/build-public-exports.mjs ." + ], + "cwd": "{projectRoot}", + "parallel": false + }, + "outputs": ["{projectRoot}/dist"] + }, + "typedoc": { + "executor": "nx:run-commands", + "options": { + "command": "pnpm exec typedoc --options packages/riviere-builder/domain-model/typedoc.json", + "cwd": "{workspaceRoot}" + }, + "dependsOn": ["build"] + } + } +} diff --git a/packages/riviere-builder/src/__fixtures__/builder-fixtures.ts b/packages/riviere-builder/domain-model/src/__fixtures__/builder-fixtures.ts similarity index 75% rename from packages/riviere-builder/src/__fixtures__/builder-fixtures.ts rename to packages/riviere-builder/domain-model/src/__fixtures__/builder-fixtures.ts index 51632baaa..8aad960ef 100644 --- a/packages/riviere-builder/src/__fixtures__/builder-fixtures.ts +++ b/packages/riviere-builder/domain-model/src/__fixtures__/builder-fixtures.ts @@ -1,6 +1,4 @@ -import type { BuilderOptions } from '../features/building/domain/builder-facade' - -export function createValidOptions(): BuilderOptions { +export function createValidOptions() { return { sources: [ { @@ -18,7 +16,7 @@ export function createValidOptions(): BuilderOptions { systemType: 'domain', }, }, - } + } as const } export function createSourceLocation() { diff --git a/packages/riviere-builder/domain-model/src/domain/api-definition.spec.ts b/packages/riviere-builder/domain-model/src/domain/api-definition.spec.ts new file mode 100644 index 000000000..59e4910f8 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/api-definition.spec.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from 'vitest' +import { ApiDefinition } from './api-definition' + +describe('ApiDefinition', () => { + it.each([ + ['rest', 'REST'], + ['GraphQL', 'GraphQL'], + ['OTHER', 'other'], + ])('normalises %s', (input, expected) => { + const result = ApiDefinition.parse(input, undefined, undefined) + + expect(result).toMatchObject({ success: true, data: { apiType: expected } }) + }) + + it('rejects an unsupported HTTP method', () => { + expect(ApiDefinition.parse('REST', 'TRACE', '/orders')).toMatchObject({ + success: false, + message: '--http-method is required for API component', + }) + }) + + it('rejects an unsupported API type', () => { + expect(ApiDefinition.parse('SOAP', undefined, '/orders')).toMatchObject({ + success: false, + message: '--api-type is required for API component', + }) + }) + + it('rejects a missing API type', () => { + expect(ApiDefinition.parse(undefined, undefined, '/orders')).toMatchObject({ + success: false, + message: '--api-type is required for API component', + }) + }) + + it('accepts a supported HTTP method', () => { + expect(ApiDefinition.parse('REST', 'POST', '/orders')).toMatchObject({ + success: true, + data: { apiType: 'REST', httpMethod: 'POST', path: '/orders' }, + }) + }) +}) diff --git a/packages/riviere-builder/domain-model/src/domain/api-definition.ts b/packages/riviere-builder/domain-model/src/domain/api-definition.ts new file mode 100644 index 000000000..08660c3b3 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/api-definition.ts @@ -0,0 +1,48 @@ +import { z } from 'zod' +import { HttpMethod } from './http-method' + +const apiTypeSchema = z.enum(['REST', 'GraphQL', 'other']) + +/** @riviere-role value-object */ +export class ApiDefinition { + declare private readonly brand: 'ApiDefinition' + + private constructor( + readonly apiType: 'REST' | 'GraphQL' | 'other', + readonly httpMethod: + | 'GET' + | 'POST' + | 'PUT' + | 'PATCH' + | 'DELETE' + | 'HEAD' + | 'OPTIONS' + | undefined, + readonly path: string | undefined, + ) {} + + static parse( + apiTypeInput: string | undefined, + httpMethodInput: string | undefined, + path: string | undefined, + ) { + const normalized = normalizeApiType(apiTypeInput) + const apiType = apiTypeSchema.safeParse(normalized) + if (!apiType.success) + return { success: false as const, message: '--api-type is required for API component' } + const httpMethod = httpMethodInput === undefined ? undefined : HttpMethod.parse(httpMethodInput) + if (httpMethod !== undefined && !httpMethod.success) + return { success: false as const, message: '--http-method is required for API component' } + return { + success: true as const, + data: new ApiDefinition(apiType.data, httpMethod?.data.value, path), + } + } +} + +function normalizeApiType(value: string | undefined): string | undefined { + if (value === undefined) return undefined + if (value.toLowerCase() === 'rest') return 'REST' + if (value.toLowerCase() === 'graphql') return 'GraphQL' + return value.toLowerCase() +} diff --git a/packages/riviere-builder/src/features/building/domain/builder-components.spec.ts b/packages/riviere-builder/domain-model/src/domain/builder-components.spec.ts similarity index 98% rename from packages/riviere-builder/src/features/building/domain/builder-components.spec.ts rename to packages/riviere-builder/domain-model/src/domain/builder-components.spec.ts index 18879e7d1..8d2741df5 100644 --- a/packages/riviere-builder/src/features/building/domain/builder-components.spec.ts +++ b/packages/riviere-builder/domain-model/src/domain/builder-components.spec.ts @@ -1,8 +1,6 @@ -import { - RiviereBuilder, type BuilderOptions -} from './builder-facade' +import { RiviereBuilder } from './builder-facade' -function createValidOptions(): BuilderOptions { +function createValidOptions() { return { sources: [ { @@ -16,7 +14,7 @@ function createValidOptions(): BuilderOptions { systemType: 'domain', }, }, - } + } as const } describe('RiviereBuilder components', () => { diff --git a/packages/riviere-builder/src/features/building/domain/builder-custom-types.spec.ts b/packages/riviere-builder/domain-model/src/domain/builder-custom-types.spec.ts similarity index 97% rename from packages/riviere-builder/src/features/building/domain/builder-custom-types.spec.ts rename to packages/riviere-builder/domain-model/src/domain/builder-custom-types.spec.ts index 4f86fe0cb..946dc1508 100644 --- a/packages/riviere-builder/src/features/building/domain/builder-custom-types.spec.ts +++ b/packages/riviere-builder/domain-model/src/domain/builder-custom-types.spec.ts @@ -1,14 +1,12 @@ -import type { RiviereGraph } from '@living-architecture/riviere-schema' -import { - RiviereBuilder, type BuilderOptions -} from './builder-facade' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' +import { RiviereBuilder } from './builder-facade' function parseGraph(builder: RiviereBuilder): RiviereGraph { const graph: RiviereGraph = JSON.parse(builder.serialize()) return graph } -function createValidOptions(): BuilderOptions { +function createValidOptions() { return { sources: [ { @@ -22,7 +20,7 @@ function createValidOptions(): BuilderOptions { systemType: 'domain', }, }, - } + } as const } describe('RiviereBuilder custom types', () => { diff --git a/packages/riviere-builder/src/features/building/domain/builder-export.spec.ts b/packages/riviere-builder/domain-model/src/domain/builder-export.spec.ts similarity index 93% rename from packages/riviere-builder/src/features/building/domain/builder-export.spec.ts rename to packages/riviere-builder/domain-model/src/domain/builder-export.spec.ts index 628076546..92fe3abd7 100644 --- a/packages/riviere-builder/src/features/building/domain/builder-export.spec.ts +++ b/packages/riviere-builder/domain-model/src/domain/builder-export.spec.ts @@ -1,10 +1,6 @@ -import { - describe, it, expect -} from 'vitest' +import { describe, it, expect } from 'vitest' import { RiviereBuilder } from './builder-facade' -import { - createValidOptions, createSourceLocation -} from '../../../__fixtures__/builder-fixtures' +import { createValidOptions, createSourceLocation } from '../__fixtures__/builder-fixtures' describe('RiviereBuilder', () => { describe('build', () => { @@ -75,9 +71,7 @@ describe('RiviereBuilder', () => { }) it('includes components', () => { - const { - graph, sourceId, targetId - } = buildValidGraph() + const { graph, sourceId, targetId } = buildValidGraph() expect(graph.components).toContainEqual( expect.objectContaining({ @@ -96,9 +90,7 @@ describe('RiviereBuilder', () => { }) it('includes links', () => { - const { - graph, sourceId, targetId - } = buildValidGraph() + const { graph, sourceId, targetId } = buildValidGraph() expect(graph.links).toContainEqual({ id: `${sourceId}->${targetId}`, @@ -186,7 +178,9 @@ describe('RiviereBuilder', () => { const graph = builder.build() - expect(graph.metadata.relationshipTypes).toStrictEqual({reads: { description: 'Reads data from the target' },}) + expect(graph.metadata.relationshipTypes).toStrictEqual({ + reads: { description: 'Reads data from the target' }, + }) }) it('excludes externalLinks when none present', () => { diff --git a/packages/riviere-builder/domain-model/src/domain/builder-facade.ts b/packages/riviere-builder/domain-model/src/domain/builder-facade.ts new file mode 100644 index 000000000..e4487ff3f --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/builder-facade.ts @@ -0,0 +1,513 @@ +import type { + APIComponent, + CustomPropertyDefinition, + CustomComponent, + DomainMetadata, + DomainOpComponent, + EventComponent, + EventHandlerComponent, + ExternalLink, + Link, + RiviereGraph, + SourceInfo, + SystemType, + UIComponent, + UseCaseComponent, +} from '@living-architecture/riviere-schema-published-language/schema' +import type { ValidationResult } from '@living-architecture/riviere-schema-published-language/graph-validation' +import { RiviereBuilder as DomainBuilder } from './riviere-builder' +import type { RiviereQuery } from './query/RiviereQuery' + +type BuilderOptions = Readonly<{ + name?: string + description?: string + sources: readonly SourceInfo[] + domains: Readonly> +}> + +type DomainInput = Readonly<{ + name: string + description: string + systemType: SystemType +}> + +type UpsertOptions = Readonly<{ noOverwrite?: boolean }> + +type UIInput = Readonly< + Pick & { + metadata?: Readonly> + } +> + +type APIInput = Readonly< + Pick< + APIComponent, + | 'name' + | 'domain' + | 'module' + | 'apiType' + | 'httpMethod' + | 'path' + | 'operationName' + | 'description' + | 'sourceLocation' + > & { metadata?: Readonly> } +> + +type UseCaseInput = Readonly< + Pick & { + metadata?: Readonly> + } +> + +type DomainOpInput = Readonly< + Pick< + DomainOpComponent, + | 'name' + | 'domain' + | 'module' + | 'operationName' + | 'entity' + | 'signature' + | 'behavior' + | 'stateChanges' + | 'businessRules' + | 'description' + | 'sourceLocation' + > & { metadata?: Readonly> } +> + +type EventInput = Readonly< + Pick< + EventComponent, + 'name' | 'domain' | 'module' | 'eventName' | 'eventSchema' | 'description' | 'sourceLocation' + > & { metadata?: Readonly> } +> + +type EventHandlerInput = Readonly< + Pick< + EventHandlerComponent, + 'name' | 'domain' | 'module' | 'subscribedEvents' | 'description' | 'sourceLocation' + > & { metadata?: Readonly> } +> + +type CustomTypeInput = Readonly<{ + name: string + description?: string + requiredProperties?: Readonly> + optionalProperties?: Readonly> +}> + +type RelationshipTypeInput = Readonly<{ + name: string + description: string +}> + +type CustomInput = Readonly< + Pick< + CustomComponent, + 'customTypeName' | 'name' | 'domain' | 'module' | 'description' | 'sourceLocation' + > & { metadata?: Readonly> } +> + +type EnrichmentInput = Readonly< + Pick +> + +type LinkInput = Readonly<{ + from: Link['source'] + to: Link['target'] + type?: Link['type'] + relationshipType?: Link['relationshipType'] + condition?: Link['condition'] + sourceLocation?: Link['sourceLocation'] +}> + +type ExternalLinkInput = Readonly<{ + from: ExternalLink['source'] + target: ExternalLink['target'] + type?: ExternalLink['type'] + description?: ExternalLink['description'] + sourceLocation?: ExternalLink['sourceLocation'] + metadata?: Readonly> +}> + +/** + * Programmatically construct Riviere architecture graphs. + * + * Thin facade preserving the flat public API while delegating + * to focused domain classes internally. + * + * @riviere-role aggregate + */ +export class RiviereBuilder { + private readonly delegate: DomainBuilder + + readonly graphPath: string + + private constructor(delegate: DomainBuilder) { + this.delegate = delegate + this.graphPath = delegate.graphPath + } + + /** + * Restores a builder from a previously serialized graph. + * + * @param graph - A valid RiviereGraph to resume from + * @param graphPath - File path where the graph is persisted + * @returns A new RiviereBuilder with the graph state restored + */ + static resume(graph: RiviereGraph, graphPath = ''): RiviereBuilder { + return new RiviereBuilder(DomainBuilder.resume(graph, graphPath)) + } + + /** + * Creates a new builder with initial configuration. + * + * @param options - Configuration including sources and domains + * @param graphPath - File path where the graph will be persisted + * @returns A new RiviereBuilder instance + */ + static new(options: BuilderOptions, graphPath = ''): RiviereBuilder { + return new RiviereBuilder(DomainBuilder.new(options, graphPath)) + } + + /** + * Adds an additional source repository to the graph. + * + * @param source - Source repository information + */ + addSource(source: SourceInfo): void { + this.delegate.construction.addSource(source) + } + + /** + * Adds a new domain to the graph. + * + * @param input - Domain name and description + */ + addDomain(input: DomainInput): void { + this.delegate.construction.addDomain(input) + } + + /** + * Adds a UI component to the graph. + * + * @param input - UI component properties + * @returns The created UI component + */ + addUI(input: UIInput): UIComponent { + return this.delegate.construction.addUI(input) + } + + /** + * Adds or updates a UI component. + * + * @param input - UI component properties + * @param options - Upsert behaviour + * @returns The component and whether it was created + */ + upsertUI( + input: UIInput, + options?: UpsertOptions, + ): { + component: UIComponent + created: boolean + } { + return this.delegate.construction.upsertUI(input, options) + } + + /** + * Adds an API component to the graph. + * + * @param input - API component properties + * @returns The created API component + */ + addApi(input: APIInput): APIComponent { + return this.delegate.construction.addApi(input) + } + + /** + * Adds or updates an API component. + * + * @param input - API component properties + * @param options - Upsert behaviour + * @returns The component and whether it was created + */ + upsertApi( + input: APIInput, + options?: UpsertOptions, + ): { + component: APIComponent + created: boolean + } { + return this.delegate.construction.upsertApi(input, options) + } + + /** + * Adds a UseCase component to the graph. + * + * @param input - UseCase component properties + * @returns The created UseCase component + */ + addUseCase(input: UseCaseInput): UseCaseComponent { + return this.delegate.construction.addUseCase(input) + } + + /** + * Adds or updates a UseCase component. + * + * @param input - UseCase component properties + * @param options - Upsert behaviour + * @returns The component and whether it was created + */ + upsertUseCase( + input: UseCaseInput, + options?: UpsertOptions, + ): { + component: UseCaseComponent + created: boolean + } { + return this.delegate.construction.upsertUseCase(input, options) + } + + /** + * Adds a DomainOp component to the graph. + * + * @param input - DomainOp component properties + * @returns The created DomainOp component + */ + addDomainOp(input: DomainOpInput): DomainOpComponent { + return this.delegate.construction.addDomainOp(input) + } + + /** + * Adds or updates a DomainOp component. + * + * @param input - DomainOp component properties + * @param options - Upsert behaviour + * @returns The component and whether it was created + */ + upsertDomainOp( + input: DomainOpInput, + options?: UpsertOptions, + ): { + component: DomainOpComponent + created: boolean + } { + return this.delegate.construction.upsertDomainOp(input, options) + } + + /** + * Adds an Event component to the graph. + * + * @param input - Event component properties + * @returns The created Event component + */ + addEvent(input: EventInput): EventComponent { + return this.delegate.construction.addEvent(input) + } + + /** + * Adds or updates an Event component. + * + * @param input - Event component properties + * @param options - Upsert behaviour + * @returns The component and whether it was created + */ + upsertEvent( + input: EventInput, + options?: UpsertOptions, + ): { + component: EventComponent + created: boolean + } { + return this.delegate.construction.upsertEvent(input, options) + } + + /** + * Adds an EventHandler component to the graph. + * + * @param input - EventHandler component properties + * @returns The created EventHandler component + */ + addEventHandler(input: EventHandlerInput): EventHandlerComponent { + return this.delegate.construction.addEventHandler(input) + } + + /** + * Adds or updates an EventHandler component. + * + * @param input - EventHandler component properties + * @param options - Upsert behaviour + * @returns The component and whether it was created + */ + upsertEventHandler( + input: EventHandlerInput, + options?: UpsertOptions, + ): { + component: EventHandlerComponent + created: boolean + } { + return this.delegate.construction.upsertEventHandler(input, options) + } + + /** + * Defines a custom component type for the graph. + * + * @param input - Custom type definition + */ + defineCustomType(input: CustomTypeInput): void { + this.delegate.construction.defineCustomType(input) + } + + /** + * Defines a relationship type for the graph. + * + * @param input - Relationship type name and description + */ + defineRelationshipType(input: RelationshipTypeInput): void { + this.delegate.construction.defineRelationshipType(input) + } + + /** + * Adds a Custom component to the graph. + * + * @param input - Custom component properties + * @returns The created Custom component + */ + addCustom(input: CustomInput): CustomComponent { + return this.delegate.construction.addCustom(input) + } + + /** + * Adds or updates a Custom component. + * + * @param input - Custom component properties + * @param options - Upsert behaviour + * @returns The component and whether it was created + */ + upsertCustom( + input: CustomInput, + options?: UpsertOptions, + ): { + component: CustomComponent + created: boolean + } { + return this.delegate.construction.upsertCustom(input, options) + } + + /** + * Enriches a DomainOp component with additional domain details. + * + * @param id - The component ID to enrich + * @param enrichment - State changes and business rules to add + */ + enrichComponent(id: string, enrichment: EnrichmentInput): void { + this.delegate.enrichment.enrichComponent(id, enrichment) + } + + /** + * Finds components similar to a query for error recovery. + * + * @param query - Search criteria including partial ID, name, type, or domain + * @param options - Optional matching thresholds and limits + * @returns Array of similar components with similarity scores + */ + nearMatches( + query: Readonly<{ + name: string + type?: import('@living-architecture/riviere-schema-published-language/schema').ComponentType + domain?: string + }>, + options?: Readonly<{ + threshold?: number + limit?: number + }>, + ) { + return this.delegate.errorRecovery.findNearMatches(query, options) + } + + /** + * Creates a link between two components in the graph. + * + * @param input - Link properties including source, target, and type + * @returns The created link + */ + link(input: LinkInput): Link { + return this.delegate.linking.link(input) + } + + /** + * Creates a link from a component to an external system. + * + * @param input - External link properties including target system info + * @returns The created external link + */ + linkExternal(input: ExternalLinkInput): ExternalLink { + return this.delegate.linking.linkExternal(input) + } + + /** + * Returns non-fatal issues found in the graph. + * + * @returns Array of warning objects with type and message + */ + warnings() { + return this.delegate.inspection.warnings() + } + + /** + * Returns statistics about the current graph state. + * + * @returns Counts of components by type, domains, and links + */ + stats() { + return this.delegate.inspection.stats() + } + + /** + * Runs full validation on the graph. + * + * @returns Validation result with valid flag and error details + */ + validate(): ValidationResult { + return this.delegate.inspection.validate() + } + + /** + * Returns IDs of components with no incoming or outgoing links. + * + * @returns Array of orphaned component IDs + */ + orphans(): string[] { + return this.delegate.inspection.orphans() + } + + /** + * Returns query capabilities for the current graph state. + * + * @returns A snapshot that can be queried without mutating the builder + */ + query(): RiviereQuery { + return this.delegate.inspection.query() + } + + /** + * Serializes the current graph state as a JSON string. + * + * @returns JSON string representation of the graph + */ + serialize(): string { + return this.delegate.serialize() + } + + /** + * Validates and returns the completed graph. + * + * @returns Valid RiviereGraph object + */ + build(): RiviereGraph { + return this.delegate.build() + } +} diff --git a/packages/riviere-builder/domain-model/src/domain/builder-graph.spec.ts b/packages/riviere-builder/domain-model/src/domain/builder-graph.spec.ts new file mode 100644 index 000000000..93687b59c --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/builder-graph.spec.ts @@ -0,0 +1,80 @@ +import { describe, expect, it } from 'vitest' +import { BuilderGraph } from './builder-graph' + +describe('BuilderGraph', () => { + it('returns a new graph without changing the original', () => { + const original = BuilderGraph.parse({ + version: '1.0', + metadata: { + generated: '2026-08-12T00:00:00.000Z', + sources: [{ repository: 'example/repository' }], + domains: { + orders: { + description: 'Orders', + systemType: 'domain', + }, + }, + customTypes: {}, + relationshipTypes: {}, + }, + components: [], + links: [], + externalLinks: [], + }) + + const updated = original.withDomain('shipping', { + description: 'Shipping', + systemType: 'domain', + }) + + expect(original.metadata.domains).not.toHaveProperty('shipping') + expect(updated.metadata.domains['shipping']).toStrictEqual({ + description: 'Shipping', + systemType: 'domain', + }) + expect(updated.metadata.generated).toBe('2026-08-12T00:00:00.000Z') + expect(updated).not.toBe(original) + }) + + it('chains updates into successive graph values', () => { + const original = BuilderGraph.parse({ + version: '1.0', + metadata: { + sources: [{ repository: 'example/repository' }], + domains: { + orders: { + description: 'Orders', + systemType: 'domain', + }, + }, + customTypes: {}, + relationshipTypes: {}, + }, + components: [], + links: [], + externalLinks: [], + }) + + const updated = original + .withDomain('shipping', { + description: 'Shipping', + systemType: 'domain', + }) + .withComponent({ + id: 'shipping:delivery:ui:tracking', + type: 'UI', + name: 'Tracking', + domain: 'shipping', + module: 'delivery', + route: '/tracking', + sourceLocation: { + repository: 'example/repository', + filePath: 'tracking.ts', + }, + }) + + expect(updated.metadata.domains).toHaveProperty('shipping') + expect(updated.components).toHaveLength(1) + expect(original.components).toHaveLength(0) + }) +}) diff --git a/packages/riviere-builder/domain-model/src/domain/builder-graph.ts b/packages/riviere-builder/domain-model/src/domain/builder-graph.ts new file mode 100644 index 000000000..c53709928 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/builder-graph.ts @@ -0,0 +1,149 @@ +import type { + Component, + CustomTypeDefinition, + DomainMetadata, + ExternalLink, + Link, + RelationshipTypeDefinition, + SourceInfo, +} from '@living-architecture/riviere-schema-published-language/schema' + +type BuilderGraphDefinition = Readonly<{ + version: string + metadata: Readonly<{ + name?: string + description?: string + generated?: string + sources: readonly SourceInfo[] + domains: Readonly> + customTypes: Readonly> + relationshipTypes: Readonly> + }> + components: readonly Component[] + links: readonly Link[] + externalLinks: readonly ExternalLink[] +}> + +/** @riviere-role value-object */ +export class BuilderGraph { + declare private readonly brand: 'BuilderGraph' + + readonly version: string + readonly metadata: BuilderGraphDefinition['metadata'] + readonly components: readonly Component[] + readonly links: readonly Link[] + readonly externalLinks: readonly ExternalLink[] + + private constructor(definition: BuilderGraphDefinition) { + this.version = definition.version + this.metadata = { + ...(definition.metadata.name !== undefined && { name: definition.metadata.name }), + ...(definition.metadata.description !== undefined && { + description: definition.metadata.description, + }), + ...(definition.metadata.generated !== undefined && { + generated: definition.metadata.generated, + }), + sources: [...definition.metadata.sources], + domains: { ...definition.metadata.domains }, + customTypes: { ...definition.metadata.customTypes }, + relationshipTypes: { ...definition.metadata.relationshipTypes }, + } + this.components = [...definition.components] + this.links = [...definition.links] + this.externalLinks = [...definition.externalLinks] + } + + static parse(definition: BuilderGraphDefinition): BuilderGraph { + return new BuilderGraph(definition) + } + + withSource(source: SourceInfo): BuilderGraph { + return BuilderGraph.parse({ + ...this.definition(), + metadata: { + ...this.metadata, + sources: [...this.metadata.sources, source], + }, + }) + } + + withDomain(name: string, domain: DomainMetadata): BuilderGraph { + return BuilderGraph.parse({ + ...this.definition(), + metadata: { + ...this.metadata, + domains: { + ...this.metadata.domains, + [name]: domain, + }, + }, + }) + } + + withCustomType(name: string, definition: CustomTypeDefinition): BuilderGraph { + return BuilderGraph.parse({ + ...this.definition(), + metadata: { + ...this.metadata, + customTypes: { + ...this.metadata.customTypes, + [name]: definition, + }, + }, + }) + } + + withRelationshipType(name: string, definition: RelationshipTypeDefinition): BuilderGraph { + return BuilderGraph.parse({ + ...this.definition(), + metadata: { + ...this.metadata, + relationshipTypes: { + ...this.metadata.relationshipTypes, + [name]: definition, + }, + }, + }) + } + + withComponent(component: Component): BuilderGraph { + return BuilderGraph.parse({ + ...this.definition(), + components: [...this.components, component], + }) + } + + withComponentAt(index: number, component: Component): BuilderGraph { + return BuilderGraph.parse({ + ...this.definition(), + components: this.components.map((existing, existingIndex) => + existingIndex === index ? component : existing, + ), + }) + } + + withLink(link: Link): BuilderGraph { + return BuilderGraph.parse({ + ...this.definition(), + links: [...this.links, link], + }) + } + + withExternalLink(link: ExternalLink): BuilderGraph { + return BuilderGraph.parse({ + ...this.definition(), + externalLinks: [...this.externalLinks, link], + }) + } + + private definition(): BuilderGraphDefinition { + return { + version: this.version, + metadata: this.metadata, + components: this.components, + links: this.links, + externalLinks: this.externalLinks, + } + } +} diff --git a/packages/riviere-builder/src/features/building/domain/builder-relationship-types.spec.ts b/packages/riviere-builder/domain-model/src/domain/builder-relationship-types.spec.ts similarity index 92% rename from packages/riviere-builder/src/features/building/domain/builder-relationship-types.spec.ts rename to packages/riviere-builder/domain-model/src/domain/builder-relationship-types.spec.ts index 4830eb666..fd8ad8150 100644 --- a/packages/riviere-builder/src/features/building/domain/builder-relationship-types.spec.ts +++ b/packages/riviere-builder/domain-model/src/domain/builder-relationship-types.spec.ts @@ -1,11 +1,7 @@ -import { - describe, expect, it -} from 'vitest' -import { - RiviereBuilder, type BuilderOptions -} from './builder-facade' +import { describe, expect, it } from 'vitest' +import { RiviereBuilder } from './builder-facade' -function createValidOptions(): BuilderOptions { +function createValidOptions() { return { sources: [{ repository: 'test/repo' }], domains: { @@ -14,7 +10,7 @@ function createValidOptions(): BuilderOptions { systemType: 'domain', }, }, - } + } as const } describe('RiviereBuilder relationship types', () => { diff --git a/packages/riviere-builder/src/features/building/domain/builder-serialize.spec.ts b/packages/riviere-builder/domain-model/src/domain/builder-serialize.spec.ts similarity index 98% rename from packages/riviere-builder/src/features/building/domain/builder-serialize.spec.ts rename to packages/riviere-builder/domain-model/src/domain/builder-serialize.spec.ts index 55c12942b..7d14dab6c 100644 --- a/packages/riviere-builder/src/features/building/domain/builder-serialize.spec.ts +++ b/packages/riviere-builder/domain-model/src/domain/builder-serialize.spec.ts @@ -1,11 +1,7 @@ -import { - describe, it, expect -} from 'vitest' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import { describe, it, expect } from 'vitest' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import { RiviereBuilder } from './builder-facade' -import { - createValidOptions, createSourceLocation -} from '../../../__fixtures__/builder-fixtures' +import { createValidOptions, createSourceLocation } from '../__fixtures__/builder-fixtures' describe('RiviereBuilder', () => { describe('serialize', () => { diff --git a/packages/riviere-builder/src/features/building/domain/builder-upsert-nested.spec.ts b/packages/riviere-builder/domain-model/src/domain/builder-upsert-nested.spec.ts similarity index 98% rename from packages/riviere-builder/src/features/building/domain/builder-upsert-nested.spec.ts rename to packages/riviere-builder/domain-model/src/domain/builder-upsert-nested.spec.ts index d0dbc1d77..33edeaf99 100644 --- a/packages/riviere-builder/src/features/building/domain/builder-upsert-nested.spec.ts +++ b/packages/riviere-builder/domain-model/src/domain/builder-upsert-nested.spec.ts @@ -1,9 +1,7 @@ -import type { RiviereGraph } from '@living-architecture/riviere-schema' -import { - RiviereBuilder, type BuilderOptions -} from './index' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' +import { RiviereBuilder } from './builder-facade' -function createValidOptions(): BuilderOptions { +function createValidOptions() { return { sources: [ { @@ -17,7 +15,7 @@ function createValidOptions(): BuilderOptions { systemType: 'domain', }, }, - } + } as const } function sourceLocation() { diff --git a/packages/riviere-builder/src/features/building/domain/builder-upsert-warnings.spec.ts b/packages/riviere-builder/domain-model/src/domain/builder-upsert-warnings.spec.ts similarity index 96% rename from packages/riviere-builder/src/features/building/domain/builder-upsert-warnings.spec.ts rename to packages/riviere-builder/domain-model/src/domain/builder-upsert-warnings.spec.ts index 29a501ee7..8027acf98 100644 --- a/packages/riviere-builder/src/features/building/domain/builder-upsert-warnings.spec.ts +++ b/packages/riviere-builder/domain-model/src/domain/builder-upsert-warnings.spec.ts @@ -1,9 +1,8 @@ -import type { RiviereGraph } from '@living-architecture/riviere-schema' -import { - ComponentTypeMismatchError, RiviereBuilder, type BuilderOptions -} from './index' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' +import { RiviereBuilder } from './builder-facade' +import { ComponentTypeMismatchError } from './construction/construction-errors' -function createValidOptions(): BuilderOptions { +function createValidOptions() { return { sources: [ { @@ -17,7 +16,7 @@ function createValidOptions(): BuilderOptions { systemType: 'domain', }, }, - } + } as const } function sourceLocation() { diff --git a/packages/riviere-builder/src/features/building/domain/builder-upsert.spec.ts b/packages/riviere-builder/domain-model/src/domain/builder-upsert.spec.ts similarity index 98% rename from packages/riviere-builder/src/features/building/domain/builder-upsert.spec.ts rename to packages/riviere-builder/domain-model/src/domain/builder-upsert.spec.ts index 1305f0d6b..0bd373ea7 100644 --- a/packages/riviere-builder/src/features/building/domain/builder-upsert.spec.ts +++ b/packages/riviere-builder/domain-model/src/domain/builder-upsert.spec.ts @@ -1,8 +1,6 @@ -import { - RiviereBuilder, type BuilderOptions -} from './index' +import { RiviereBuilder } from './builder-facade' -function createValidOptions(): BuilderOptions { +function createValidOptions() { return { sources: [ { @@ -16,7 +14,7 @@ function createValidOptions(): BuilderOptions { systemType: 'domain', }, }, - } + } as const } function sourceLocation() { diff --git a/packages/riviere-builder/src/features/building/domain/builder-validation.spec.ts b/packages/riviere-builder/domain-model/src/domain/builder-validation.spec.ts similarity index 96% rename from packages/riviere-builder/src/features/building/domain/builder-validation.spec.ts rename to packages/riviere-builder/domain-model/src/domain/builder-validation.spec.ts index 3af34cab5..512b7fa45 100644 --- a/packages/riviere-builder/src/features/building/domain/builder-validation.spec.ts +++ b/packages/riviere-builder/domain-model/src/domain/builder-validation.spec.ts @@ -1,10 +1,6 @@ -import { - describe, it, expect -} from 'vitest' +import { describe, it, expect } from 'vitest' import { RiviereBuilder } from './builder-facade' -import { - createValidOptions, createSourceLocation -} from '../../../__fixtures__/builder-fixtures' +import { createValidOptions, createSourceLocation } from '../__fixtures__/builder-fixtures' describe('RiviereBuilder', () => { describe('validate', () => { diff --git a/packages/riviere-builder/src/features/building/domain/builder.spec.ts b/packages/riviere-builder/domain-model/src/domain/builder.spec.ts similarity index 94% rename from packages/riviere-builder/src/features/building/domain/builder.spec.ts rename to packages/riviere-builder/domain-model/src/domain/builder.spec.ts index e278e66f4..2dfd8ddbb 100644 --- a/packages/riviere-builder/src/features/building/domain/builder.spec.ts +++ b/packages/riviere-builder/domain-model/src/domain/builder.spec.ts @@ -1,14 +1,12 @@ -import type { RiviereGraph } from '@living-architecture/riviere-schema' -import { - RiviereBuilder, type BuilderOptions -} from './builder-facade' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' +import { RiviereBuilder } from './builder-facade' function parseGraph(builder: RiviereBuilder): RiviereGraph { const graph: RiviereGraph = JSON.parse(builder.serialize()) return graph } -function createValidOptions(): BuilderOptions { +function createValidOptions() { return { sources: [ { @@ -22,13 +20,13 @@ function createValidOptions(): BuilderOptions { systemType: 'domain', }, }, - } + } as const } describe('RiviereBuilder', () => { describe('new', () => { it('returns builder instance when given valid options', () => { - const options: BuilderOptions = { + const options = { sources: [ { repository: 'my-org/my-repo', @@ -41,7 +39,7 @@ describe('RiviereBuilder', () => { systemType: 'domain', }, }, - } + } as const const builder = RiviereBuilder.new(options) @@ -49,7 +47,7 @@ describe('RiviereBuilder', () => { }) it('throws when sources array is empty', () => { - const options: BuilderOptions = { + const options = { sources: [], domains: { orders: { @@ -57,22 +55,22 @@ describe('RiviereBuilder', () => { systemType: 'domain', }, }, - } + } as const expect(() => RiviereBuilder.new(options)).toThrow('At least one source required') }) it('throws when domains object is empty', () => { - const options: BuilderOptions = { + const options = { sources: [{ repository: 'my-org/my-repo' }], domains: {}, - } + } as const expect(() => RiviereBuilder.new(options)).toThrow('At least one domain required') }) it('configures graph metadata from options', () => { - const options: BuilderOptions = { + const options = { name: 'my-service', description: 'Service description', sources: [ @@ -87,7 +85,7 @@ describe('RiviereBuilder', () => { systemType: 'domain', }, }, - } + } as const const builder = RiviereBuilder.new(options) diff --git a/packages/riviere-builder/src/platform/domain/collection-utils/deduplicate-strings.ts b/packages/riviere-builder/domain-model/src/domain/collection-utils/deduplicate-strings.ts similarity index 100% rename from packages/riviere-builder/src/platform/domain/collection-utils/deduplicate-strings.ts rename to packages/riviere-builder/domain-model/src/domain/collection-utils/deduplicate-strings.ts diff --git a/packages/riviere-builder/src/platform/domain/collection-utils/deduplicate.spec.ts b/packages/riviere-builder/domain-model/src/domain/collection-utils/deduplicate.spec.ts similarity index 100% rename from packages/riviere-builder/src/platform/domain/collection-utils/deduplicate.spec.ts rename to packages/riviere-builder/domain-model/src/domain/collection-utils/deduplicate.spec.ts diff --git a/packages/riviere-builder/domain-model/src/domain/component-definition.ts b/packages/riviere-builder/domain-model/src/domain/component-definition.ts new file mode 100644 index 000000000..f061afb9b --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/component-definition.ts @@ -0,0 +1,190 @@ +import type { Component } from '@living-architecture/riviere-schema-published-language/schema' +import { ComponentType } from './component-type' +import { ApiDefinition } from './api-definition' +import { CustomComponentDefinition } from './custom-component-definition' +import { SubscribedEvents } from './subscribed-events' + +interface ComponentDefinitionInput { + componentType: string + name: string + domain: string + module: string + repository: string + filePath: string + lineNumber?: number + columnNumber?: number + route?: string + apiType?: string + httpMethod?: string + httpPath?: string + operationName?: string + entity?: string + eventName?: string + eventSchema?: string + subscribedEvents?: string + customType?: string + customProperty?: string[] + description?: string +} + +type CommonInput = Pick +type ComponentDefinitionValue = + | { type: 'UI'; input: CommonInput & { route: string } } + | { + type: 'API' + input: CommonInput & { + apiType: ApiDefinition['apiType'] + httpMethod?: NonNullable + path?: string + } + } + | { type: 'UseCase'; input: CommonInput } + | { type: 'DomainOp'; input: CommonInput & { operationName: string; entity?: string } } + | { type: 'Event'; input: CommonInput & { eventName: string; eventSchema?: string } } + | { type: 'EventHandler'; input: CommonInput & { subscribedEvents: string[] } } + | { + type: 'Custom' + input: CommonInput & { + customTypeName: string + metadata?: NonNullable + } + } + +type ParsedValue = + | { success: true; data: ComponentDefinitionValue } + | { success: false; message: string } + +/** @riviere-role value-object */ +export class ComponentDefinition { + declare private readonly brand: 'ComponentDefinition' + + private constructor(readonly value: ComponentDefinitionValue) {} + + static parse(input: ComponentDefinitionInput) { + const componentType = ComponentType.parse(input.componentType) + if (!componentType.success) return invalid(`Invalid component type: ${input.componentType}`) + const parsed = parseValue(componentType.data.value, commonInput(input), input) + return parsed.success + ? { success: true as const, data: new ComponentDefinition(parsed.data) } + : parsed + } +} + +function parseValue( + type: ComponentDefinitionValue['type'], + common: CommonInput, + input: ComponentDefinitionInput, +): ParsedValue { + switch (type) { + case 'UI': + return required(input.route, '--route is required for UI component', (route) => ({ + type, + input: { ...common, route }, + })) + case 'API': + return parseApi(common, input) + case 'UseCase': + return valid({ type, input: common }) + case 'DomainOp': + return required( + input.operationName, + '--operation-name is required for DomainOp component', + (operationName) => ({ + type, + input: { + ...common, + operationName, + ...(input.entity === undefined ? {} : { entity: input.entity }), + }, + }), + ) + case 'Event': + return required( + input.eventName, + '--event-name is required for Event component', + (eventName) => ({ + type, + input: { + ...common, + eventName, + ...(input.eventSchema === undefined ? {} : { eventSchema: input.eventSchema }), + }, + }), + ) + case 'EventHandler': + return parseEventHandler(common, input.subscribedEvents) + case 'Custom': + return parseCustom(common, input) + } +} + +function parseApi(common: CommonInput, input: ComponentDefinitionInput): ParsedValue { + const parsed = ApiDefinition.parse(input.apiType, input.httpMethod, input.httpPath) + return parsed.success + ? valid({ + type: 'API', + input: { + ...common, + apiType: parsed.data.apiType, + ...(parsed.data.httpMethod === undefined ? {} : { httpMethod: parsed.data.httpMethod }), + ...(parsed.data.path === undefined ? {} : { path: parsed.data.path }), + }, + }) + : parsed +} + +function parseEventHandler(common: CommonInput, value: string | undefined): ParsedValue { + const parsed = SubscribedEvents.parse(value) + return parsed.success + ? valid({ + type: 'EventHandler', + input: { ...common, subscribedEvents: [...parsed.data.values] }, + }) + : parsed +} + +function parseCustom(common: CommonInput, input: ComponentDefinitionInput): ParsedValue { + const parsed = CustomComponentDefinition.parse(input.customType, input.customProperty) + return parsed.success + ? valid({ + type: 'Custom', + input: { + ...common, + customTypeName: parsed.data.customTypeName, + ...(parsed.data.metadata === undefined ? {} : { metadata: parsed.data.metadata }), + }, + }) + : parsed +} + +function commonInput(input: ComponentDefinitionInput): CommonInput { + return { + name: input.name, + domain: input.domain, + module: input.module, + sourceLocation: { + repository: input.repository, + filePath: input.filePath, + ...(input.lineNumber === undefined ? {} : { lineNumber: input.lineNumber }), + ...(input.columnNumber === undefined ? {} : { columnNumber: input.columnNumber }), + }, + ...(input.description === undefined ? {} : { description: input.description }), + } +} + +function required( + value: string | undefined, + message: string, + create: (value: string) => ComponentDefinitionValue, +): ParsedValue { + return value === undefined || value.trim().length === 0 + ? invalid(message) + : valid(create(value.trim())) +} + +function valid(data: ComponentDefinitionValue): ParsedValue { + return { success: true, data } +} +function invalid(message: string) { + return { success: false as const, message } +} diff --git a/packages/riviere-builder/domain-model/src/domain/component-type.spec.ts b/packages/riviere-builder/domain-model/src/domain/component-type.spec.ts new file mode 100644 index 000000000..98d87ec74 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/component-type.spec.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from 'vitest' +import { ComponentType } from './component-type' + +describe('ComponentType', () => { + it.each([ + ['UI', 'UI'], + ['api', 'API'], + ['usecase', 'UseCase'], + ['DomainOp', 'DomainOp'], + ['event', 'Event'], + ['eventhandler', 'EventHandler'], + ['custom', 'Custom'], + ])('parses %s as %s', (value, expected) => { + const result = ComponentType.parse(value) + + expect(result.success).toBe(true) + expect(result.success && result.data.value).toBe(expected) + expect(result.success && result.data.componentIdValue).toBe(expected.toLowerCase()) + }) + + it('returns the validation error for an unsupported component type', () => { + const result = ComponentType.parse('other') + + expect(result.success).toBe(false) + expect(!result.success && result.error.issues).not.toHaveLength(0) + }) +}) diff --git a/packages/riviere-builder/domain-model/src/domain/component-type.ts b/packages/riviere-builder/domain-model/src/domain/component-type.ts new file mode 100644 index 000000000..ced7936a6 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/component-type.ts @@ -0,0 +1,38 @@ +import { z } from 'zod' + +const componentTypes = [ + 'UI', + 'API', + 'UseCase', + 'DomainOp', + 'Event', + 'EventHandler', + 'Custom', +] as const +const componentTypeSchema = z.enum(componentTypes) +type ComponentTypeValue = z.infer + +/** @riviere-role value-object */ +export class ComponentType { + declare private readonly brand: 'ComponentType' + readonly componentIdValue: string + readonly value: ComponentTypeValue + + private constructor(value: ComponentTypeValue) { + this.componentIdValue = value.toLowerCase() + this.value = value + } + + static parse(value: string) { + const canonicalValue = componentTypes.find( + (componentType) => componentType.toLowerCase() === value.toLowerCase(), + ) + const parsed = componentTypeSchema.safeParse(canonicalValue) + return parsed.success + ? { + data: new ComponentType(parsed.data), + success: true as const, + } + : parsed + } +} diff --git a/packages/riviere-builder/src/features/building/domain/construction/builder-assertions.ts b/packages/riviere-builder/domain-model/src/domain/construction/builder-assertions.ts similarity index 76% rename from packages/riviere-builder/src/features/building/domain/construction/builder-assertions.ts rename to packages/riviere-builder/domain-model/src/domain/construction/builder-assertions.ts index 9ac57dec9..dbdddcfbb 100644 --- a/packages/riviere-builder/src/features/building/domain/construction/builder-assertions.ts +++ b/packages/riviere-builder/domain-model/src/domain/construction/builder-assertions.ts @@ -1,6 +1,7 @@ import type { - CustomTypeDefinition, DomainMetadata -} from '@living-architecture/riviere-schema' + CustomTypeDefinition, + DomainMetadata, +} from '@living-architecture/riviere-schema-published-language/schema' import { CustomTypeNotFoundError, DomainNotFoundError, @@ -8,7 +9,10 @@ import { } from './construction-errors' /** @riviere-role domain-service */ -export function assertDomainExists(domains: Record, domain: string): void { +export function assertDomainExists( + domains: Readonly>, + domain: string, +): void { if (!domains[domain]) { throw new DomainNotFoundError(domain) } @@ -16,7 +20,7 @@ export function assertDomainExists(domains: Record, doma /** @riviere-role domain-service */ export function assertCustomTypeExists( - customTypes: Record, + customTypes: Readonly>, customTypeName: string, ): void { if (!customTypes[customTypeName]) { @@ -27,7 +31,7 @@ export function assertCustomTypeExists( /** @riviere-role domain-service */ export function assertRequiredPropertiesProvided( - customTypes: Record, + customTypes: Readonly>, customTypeName: string, metadata: Record | undefined, ): void { diff --git a/packages/riviere-builder/domain-model/src/domain/construction/builder-internals.ts b/packages/riviere-builder/domain-model/src/domain/construction/builder-internals.ts new file mode 100644 index 000000000..9ded59387 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/construction/builder-internals.ts @@ -0,0 +1,56 @@ +import type { + Component, + CustomTypeDefinition, + DomainMetadata, +} from '@living-architecture/riviere-schema-published-language/schema' +import { ComponentId } from '@living-architecture/riviere-schema-published-language/component-id' +import { createSourceNotFoundError } from '../error-recovery/component-suggestion' +import { ComponentNotFoundError } from './construction-errors' +import { + assertCustomTypeExists, + assertDomainExists, + assertRequiredPropertiesProvided, +} from './builder-assertions' + +/** @riviere-role domain-service */ +export function generateComponentId( + domain: string, + module: string, + type: string, + name: string, +): string { + const nameSegment = name.toLowerCase().replaceAll(/\s+/g, '-') + return `${domain}:${module}:${type}:${nameSegment}` +} + +/** @riviere-role domain-service */ +export function createComponentNotFoundError(components: readonly Component[], id: string): Error { + const parsed = ComponentId.parse(id) + if (!parsed.success) return new ComponentNotFoundError(id, []) + return createSourceNotFoundError(components, parsed.componentId) +} + +/** @riviere-role domain-service */ +export function validateDomainExists( + domains: Readonly>, + domain: string, +): void { + assertDomainExists(domains, domain) +} + +/** @riviere-role domain-service */ +export function validateCustomType( + customTypes: Readonly>, + customTypeName: string, +): void { + assertCustomTypeExists(customTypes, customTypeName) +} + +/** @riviere-role domain-service */ +export function validateRequiredProperties( + customTypes: Readonly>, + customTypeName: string, + metadata: Record | undefined, +): void { + assertRequiredPropertiesProvided(customTypes, customTypeName, metadata) +} diff --git a/packages/riviere-builder/domain-model/src/domain/construction/component-registration.ts b/packages/riviere-builder/domain-model/src/domain/construction/component-registration.ts new file mode 100644 index 000000000..7b35251cc --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/construction/component-registration.ts @@ -0,0 +1,74 @@ +import type { Component } from '@living-architecture/riviere-schema-published-language/schema' +import type { BuilderGraph } from '../builder-graph' +import { ComponentTypeMismatchError, DuplicateComponentError } from './construction-errors' +import { mergeComponentForUpsert } from '../enrichment/upsert-merge' + +type AddScalarOverwriteWarning = ( + warning: Readonly<{ + code: 'SCALAR_OVERWRITE' + message: string + componentId: string + field: string + oldValue: string | number | boolean + newValue: string | number | boolean + }>, +) => void + +/** @riviere-role domain-service */ +export function registerComponent( + graph: BuilderGraph, + component: T, +): Readonly<{ + graph: BuilderGraph + component: T +}> { + if (graph.components.some((existing) => existing.id === component.id)) { + throw new DuplicateComponentError(component.id) + } + + return { + graph: graph.withComponent(component), + component, + } +} + +/** @riviere-role domain-service */ +export function upsertComponent( + graph: BuilderGraph, + incoming: T, + options: Readonly<{ noOverwrite?: boolean }> | undefined, + addWarning: AddScalarOverwriteWarning, +): Readonly<{ + graph: BuilderGraph + component: T + created: boolean +}> { + const existingIndex = graph.components.findIndex((component) => component.id === incoming.id) + if (existingIndex === -1) { + return { + graph: graph.withComponent(incoming), + component: incoming, + created: true, + } + } + + const existing = graph.components[existingIndex] + if (!isSameTypeComponent(existing, incoming)) { + throw new ComponentTypeMismatchError(incoming.id, existing?.type ?? 'unknown', incoming.type) + } + + const component = mergeComponentForUpsert(existing, incoming, options, addWarning) + + return { + graph: graph.withComponentAt(existingIndex, component), + component, + created: false, + } +} + +function isSameTypeComponent( + existing: Component | undefined, + incoming: T, +): existing is T { + return existing?.type === incoming.type +} diff --git a/packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts b/packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts similarity index 100% rename from packages/riviere-builder/src/features/building/domain/construction/construction-errors.ts rename to packages/riviere-builder/domain-model/src/domain/construction/construction-errors.ts diff --git a/packages/riviere-builder/domain-model/src/domain/construction/errors.spec.ts b/packages/riviere-builder/domain-model/src/domain/construction/errors.spec.ts new file mode 100644 index 000000000..cb2b81a23 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/construction/errors.spec.ts @@ -0,0 +1,226 @@ +import { describe, it, expect } from 'vitest' +import { + ComponentNotFoundError, + CustomTypeNotFoundError, + DomainNotFoundError, + DuplicateComponentError, + DuplicateDomainError, + SourceConflictError, + ComponentTypeMismatchError, + CustomTypeAlreadyDefinedError, + MissingRequiredPropertiesError, + InvalidGraphError, + MissingSourcesError, + MissingDomainsError, + BuildValidationError, + DuplicateLinkError, + RelationshipTypeAlreadyDefinedError, + RelationshipTypeNotFoundError, +} from './construction-errors' +import { InvalidEnrichmentTargetError } from '../enrichment/enrichment-errors' + +describe('errors', () => { + describe('DuplicateDomainError', () => { + it('includes domain name in message', () => { + const error = new DuplicateDomainError('orders') + + expect(error.message).toBe("Domain 'orders' already exists") + expect(error.domainName).toBe('orders') + expect(error.name).toBe('DuplicateDomainError') + }) + }) + + describe('SourceConflictError', () => { + it('includes repository in message', () => { + const error = new SourceConflictError('test/repo') + + expect(error.message).toBe("Source 'test/repo' already exists with different values") + expect(error.repository).toBe('test/repo') + expect(error.name).toBe('SourceConflictError') + }) + }) + + describe('DomainNotFoundError', () => { + it('includes domain name in message', () => { + const error = new DomainNotFoundError('orders') + + expect(error.message).toBe("Domain 'orders' does not exist") + expect(error.domainName).toBe('orders') + expect(error.name).toBe('DomainNotFoundError') + }) + }) + + describe('CustomTypeNotFoundError', () => { + it('includes custom type name and defined types in message', () => { + const error = new CustomTypeNotFoundError('Queue', ['Worker', 'Cache']) + + expect(error.message).toBe("Custom type 'Queue' not defined. Defined types: Worker, Cache") + expect(error.customTypeName).toBe('Queue') + expect(error.definedTypes).toStrictEqual(['Worker', 'Cache']) + expect(error.name).toBe('CustomTypeNotFoundError') + }) + + it('handles empty defined types', () => { + const error = new CustomTypeNotFoundError('Queue', []) + + expect(error.message).toBe( + "Custom type 'Queue' not defined. No custom types have been defined.", + ) + }) + }) + + describe('DuplicateComponentError', () => { + it('includes component ID in message', () => { + const error = new DuplicateComponentError('orders:checkout:api:create-order') + + expect(error.message).toBe( + "Component with ID 'orders:checkout:api:create-order' already exists", + ) + expect(error.componentId).toBe('orders:checkout:api:create-order') + expect(error.name).toBe('DuplicateComponentError') + }) + }) + + describe('ComponentTypeMismatchError', () => { + it('includes component identity and types in message', () => { + const error = new ComponentTypeMismatchError('orders:checkout:ui:checkout-page', 'UI', 'API') + + expect(error.message).toBe( + "Component 'orders:checkout:ui:checkout-page' already exists as type 'UI'; cannot upsert as 'API'", + ) + expect(error.componentId).toBe('orders:checkout:ui:checkout-page') + expect(error.existingType).toBe('UI') + expect(error.incomingType).toBe('API') + }) + }) + + describe('ComponentNotFoundError', () => { + it('includes component ID and empty suggestions by default', () => { + const error = new ComponentNotFoundError('orders:checkout:api:create-ordr') + + expect(error.message).toBe("Source component 'orders:checkout:api:create-ordr' not found") + expect(error.componentId).toBe('orders:checkout:api:create-ordr') + expect(error.suggestions).toStrictEqual([]) + expect(error.name).toBe('ComponentNotFoundError') + }) + + it('includes suggestions in message when provided', () => { + const error = new ComponentNotFoundError('orders:checkout:api:create-ordr', [ + 'orders:checkout:api:create-order', + 'orders:checkout:api:update-order', + ]) + + expect(error.message).toBe( + "Source component 'orders:checkout:api:create-ordr' not found. Did you mean: orders:checkout:api:create-order, orders:checkout:api:update-order?", + ) + expect(error.suggestions).toStrictEqual([ + 'orders:checkout:api:create-order', + 'orders:checkout:api:update-order', + ]) + }) + }) + + describe('InvalidEnrichmentTargetError', () => { + it('includes component ID and type in message', () => { + const error = new InvalidEnrichmentTargetError('orders:api:create', 'API') + + expect(error.message).toBe( + "Only DomainOp components can be enriched. 'orders:api:create' is type 'API'", + ) + expect(error.componentId).toBe('orders:api:create') + expect(error.componentType).toBe('API') + expect(error.name).toBe('InvalidEnrichmentTargetError') + }) + }) + + describe('CustomTypeAlreadyDefinedError', () => { + it('includes type name in message', () => { + const error = new CustomTypeAlreadyDefinedError('Worker') + + expect(error.message).toBe("Custom type 'Worker' already defined") + expect(error.typeName).toBe('Worker') + expect(error.name).toBe('CustomTypeAlreadyDefinedError') + }) + }) + + describe('RelationshipTypeAlreadyDefinedError', () => { + it('includes the relationship type name in the message', () => { + const error = new RelationshipTypeAlreadyDefinedError('reads') + + expect(error.message).toBe("Relationship type 'reads' already defined") + expect(error.typeName).toBe('reads') + expect(error.name).toBe('RelationshipTypeAlreadyDefinedError') + }) + }) + + describe('RelationshipTypeNotFoundError', () => { + it('includes the relationship type and available types in the message', () => { + const error = new RelationshipTypeNotFoundError('queries', ['reads', 'writes']) + + expect(error.message).toBe( + "Relationship type 'queries' not defined. Defined types: reads, writes", + ) + expect(error.relationshipType).toBe('queries') + expect(error.definedTypes).toStrictEqual(['reads', 'writes']) + expect(error.name).toBe('RelationshipTypeNotFoundError') + }) + }) + + describe('DuplicateLinkError', () => { + it('includes the Link ID in the message', () => { + const error = new DuplicateLinkError('source->target@file.sql:12:5') + + expect(error.message).toBe("Link with ID 'source->target@file.sql:12:5' already exists") + expect(error.linkId).toBe('source->target@file.sql:12:5') + expect(error.name).toBe('DuplicateLinkError') + }) + }) + + describe('MissingRequiredPropertiesError', () => { + it('includes custom type name and missing keys in message', () => { + const error = new MissingRequiredPropertiesError('Worker', ['queueName', 'concurrency']) + + expect(error.message).toBe("Missing required properties for 'Worker': queueName, concurrency") + expect(error.customTypeName).toBe('Worker') + expect(error.missingKeys).toStrictEqual(['queueName', 'concurrency']) + expect(error.name).toBe('MissingRequiredPropertiesError') + }) + }) + + describe('InvalidGraphError', () => { + it('includes reason in message', () => { + const error = new InvalidGraphError('missing version') + + expect(error.message).toBe('Invalid graph: missing version') + expect(error.name).toBe('InvalidGraphError') + }) + }) + + describe('MissingSourcesError', () => { + it('sets message', () => { + const error = new MissingSourcesError() + + expect(error.message).toBe('At least one source required') + expect(error.name).toBe('MissingSourcesError') + }) + }) + + describe('MissingDomainsError', () => { + it('sets message', () => { + const error = new MissingDomainsError() + + expect(error.message).toBe('At least one domain required') + expect(error.name).toBe('MissingDomainsError') + }) + }) + + describe('BuildValidationError', () => { + it('includes validation messages in message', () => { + const error = new BuildValidationError(['error 1', 'error 2']) + + expect(error.message).toBe('Validation failed: error 1; error 2') + expect(error.validationMessages).toStrictEqual(['error 1', 'error 2']) + expect(error.name).toBe('BuildValidationError') + }) + }) +}) diff --git a/packages/riviere-builder/domain-model/src/domain/construction/graph-construction.ts b/packages/riviere-builder/domain-model/src/domain/construction/graph-construction.ts new file mode 100644 index 000000000..b6c2773b9 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/construction/graph-construction.ts @@ -0,0 +1,453 @@ +import type { + APIComponent, + Component, + CustomPropertyDefinition, + CustomComponent, + DomainOpComponent, + EventComponent, + EventHandlerComponent, + SourceInfo, + SystemType, + UIComponent, + UseCaseComponent, +} from '@living-architecture/riviere-schema-published-language/schema' +import type { BuilderGraph } from '../builder-graph' +import { + CustomTypeAlreadyDefinedError, + DuplicateDomainError, + SourceConflictError, + RelationshipTypeAlreadyDefinedError, +} from './construction-errors' +import { + generateComponentId, + validateCustomType, + validateDomainExists, + validateRequiredProperties, +} from './builder-internals' +import { registerComponent, upsertComponent } from './component-registration' + +type AddScalarOverwriteWarning = ( + warning: Readonly<{ + code: 'SCALAR_OVERWRITE' + message: string + componentId: string + field: string + oldValue: string | number | boolean + newValue: string | number | boolean + }>, +) => void + +type DomainInput = Readonly<{ + name: string + description: string + systemType: SystemType +}> + +type UpsertOptions = Readonly<{ noOverwrite?: boolean }> + +type UIInput = Readonly< + Pick & { + metadata?: Readonly> + } +> + +type APIInput = Readonly< + Pick< + APIComponent, + | 'name' + | 'domain' + | 'module' + | 'apiType' + | 'httpMethod' + | 'path' + | 'operationName' + | 'description' + | 'sourceLocation' + > & { metadata?: Readonly> } +> + +type UseCaseInput = Readonly< + Pick & { + metadata?: Readonly> + } +> + +type DomainOpInput = Readonly< + Pick< + DomainOpComponent, + | 'name' + | 'domain' + | 'module' + | 'operationName' + | 'entity' + | 'signature' + | 'behavior' + | 'stateChanges' + | 'businessRules' + | 'description' + | 'sourceLocation' + > & { metadata?: Readonly> } +> + +type EventInput = Readonly< + Pick< + EventComponent, + 'name' | 'domain' | 'module' | 'eventName' | 'eventSchema' | 'description' | 'sourceLocation' + > & { metadata?: Readonly> } +> + +type EventHandlerInput = Readonly< + Pick< + EventHandlerComponent, + 'name' | 'domain' | 'module' | 'subscribedEvents' | 'description' | 'sourceLocation' + > & { metadata?: Readonly> } +> + +type CustomTypeInput = Readonly<{ + name: string + description?: string + requiredProperties?: Readonly> + optionalProperties?: Readonly> +}> + +type RelationshipTypeInput = Readonly<{ + name: string + description: string +}> + +type CustomInput = Readonly< + Pick< + CustomComponent, + 'customTypeName' | 'name' | 'domain' | 'module' | 'description' | 'sourceLocation' + > & { metadata?: Readonly> } +> + +/** @riviere-role domain-service */ +export class GraphConstruction { + constructor( + private graph: BuilderGraph, + private readonly addWarning: AddScalarOverwriteWarning, + private readonly updateGraph: (graph: BuilderGraph) => void, + ) {} + + addSource(source: SourceInfo): void { + const existing = this.graph.metadata.sources.find( + (item) => item.repository === source.repository, + ) + if (existing) { + if ( + existing.repository === source.repository && + existing.commit === source.commit && + existing.extractedAt === source.extractedAt + ) { + return + } + + throw new SourceConflictError(source.repository) + } + + this.replaceGraph(this.graph.withSource(source)) + } + + addDomain(input: DomainInput): void { + const existing = this.graph.metadata.domains[input.name] + if (existing) { + if (existing.description === input.description && existing.systemType === input.systemType) { + return + } + + throw new DuplicateDomainError(input.name) + } + + this.replaceGraph( + this.graph.withDomain(input.name, { + description: input.description, + systemType: input.systemType, + }), + ) + } + + addUI(input: UIInput): UIComponent { + return this.registerComponent(this.buildUIComponent(input)) + } + + upsertUI( + input: UIInput, + options?: UpsertOptions, + ): { + component: UIComponent + created: boolean + } { + return this.upsertTypedComponent(this.buildUIComponent(input), options) + } + + addApi(input: APIInput): APIComponent { + return this.registerComponent(this.buildAPIComponent(input)) + } + + upsertApi( + input: APIInput, + options?: UpsertOptions, + ): { + component: APIComponent + created: boolean + } { + return this.upsertTypedComponent(this.buildAPIComponent(input), options) + } + + addUseCase(input: UseCaseInput): UseCaseComponent { + return this.registerComponent(this.buildUseCaseComponent(input)) + } + + upsertUseCase( + input: UseCaseInput, + options?: UpsertOptions, + ): { + component: UseCaseComponent + created: boolean + } { + return this.upsertTypedComponent(this.buildUseCaseComponent(input), options) + } + + addDomainOp(input: DomainOpInput): DomainOpComponent { + return this.registerComponent(this.buildDomainOpComponent(input)) + } + + upsertDomainOp( + input: DomainOpInput, + options?: UpsertOptions, + ): { + component: DomainOpComponent + created: boolean + } { + return this.upsertTypedComponent(this.buildDomainOpComponent(input), options) + } + + addEvent(input: EventInput): EventComponent { + return this.registerComponent(this.buildEventComponent(input)) + } + + upsertEvent( + input: EventInput, + options?: UpsertOptions, + ): { + component: EventComponent + created: boolean + } { + return this.upsertTypedComponent(this.buildEventComponent(input), options) + } + + addEventHandler(input: EventHandlerInput): EventHandlerComponent { + return this.registerComponent(this.buildEventHandlerComponent(input)) + } + + upsertEventHandler( + input: EventHandlerInput, + options?: UpsertOptions, + ): { + component: EventHandlerComponent + created: boolean + } { + return this.upsertTypedComponent(this.buildEventHandlerComponent(input), options) + } + + defineCustomType(input: CustomTypeInput): void { + const customTypes = this.graph.metadata.customTypes + + if (customTypes[input.name]) { + throw new CustomTypeAlreadyDefinedError(input.name) + } + + this.replaceGraph( + this.graph.withCustomType(input.name, { + ...(input.requiredProperties !== undefined && { + requiredProperties: input.requiredProperties, + }), + ...(input.optionalProperties !== undefined && { + optionalProperties: input.optionalProperties, + }), + ...(input.description !== undefined && { description: input.description }), + }), + ) + } + + defineRelationshipType(input: RelationshipTypeInput): void { + const relationshipTypes = this.graph.metadata.relationshipTypes + if (Object.hasOwn(relationshipTypes, input.name)) { + throw new RelationshipTypeAlreadyDefinedError(input.name) + } + + this.replaceGraph( + this.graph.withRelationshipType(input.name, { description: input.description }), + ) + } + + addCustom(input: CustomInput): CustomComponent { + return this.registerComponent(this.buildCustomComponent(input)) + } + + upsertCustom( + input: CustomInput, + options?: UpsertOptions, + ): { + component: CustomComponent + created: boolean + } { + return this.upsertTypedComponent(this.buildCustomComponent(input), options) + } + + private buildUIComponent(input: UIInput): UIComponent { + validateDomainExists(this.graph.metadata.domains, input.domain) + const id = generateComponentId(input.domain, input.module, 'ui', input.name) + + return { + id, + type: 'UI', + name: input.name, + domain: input.domain, + module: input.module, + route: input.route, + sourceLocation: input.sourceLocation, + ...(input.description !== undefined && { description: input.description }), + } + } + + private buildAPIComponent(input: APIInput): APIComponent { + validateDomainExists(this.graph.metadata.domains, input.domain) + const id = generateComponentId(input.domain, input.module, 'api', input.name) + + return { + id, + type: 'API', + name: input.name, + domain: input.domain, + module: input.module, + apiType: input.apiType, + sourceLocation: input.sourceLocation, + ...(input.httpMethod !== undefined && { httpMethod: input.httpMethod }), + ...(input.path !== undefined && { path: input.path }), + ...(input.operationName !== undefined && { operationName: input.operationName }), + ...(input.description !== undefined && { description: input.description }), + } + } + + private buildUseCaseComponent(input: UseCaseInput): UseCaseComponent { + validateDomainExists(this.graph.metadata.domains, input.domain) + const id = generateComponentId(input.domain, input.module, 'usecase', input.name) + + return { + id, + type: 'UseCase', + name: input.name, + domain: input.domain, + module: input.module, + sourceLocation: input.sourceLocation, + ...(input.description !== undefined && { description: input.description }), + } + } + + private buildDomainOpComponent(input: DomainOpInput): DomainOpComponent { + validateDomainExists(this.graph.metadata.domains, input.domain) + const id = generateComponentId(input.domain, input.module, 'domainop', input.name) + + return { + id, + type: 'DomainOp', + name: input.name, + domain: input.domain, + module: input.module, + operationName: input.operationName, + sourceLocation: input.sourceLocation, + ...(input.entity !== undefined && { entity: input.entity }), + ...(input.signature !== undefined && { signature: input.signature }), + ...(input.behavior !== undefined && { behavior: input.behavior }), + ...(input.stateChanges !== undefined && { stateChanges: [...input.stateChanges] }), + ...(input.businessRules !== undefined && { businessRules: [...input.businessRules] }), + ...(input.description !== undefined && { description: input.description }), + } + } + + private buildEventComponent(input: EventInput): EventComponent { + validateDomainExists(this.graph.metadata.domains, input.domain) + const id = generateComponentId(input.domain, input.module, 'event', input.name) + + return { + id, + type: 'Event', + name: input.name, + domain: input.domain, + module: input.module, + eventName: input.eventName, + sourceLocation: input.sourceLocation, + ...(input.eventSchema !== undefined && { eventSchema: input.eventSchema }), + ...(input.description !== undefined && { description: input.description }), + } + } + + private buildEventHandlerComponent(input: EventHandlerInput): EventHandlerComponent { + validateDomainExists(this.graph.metadata.domains, input.domain) + const id = generateComponentId(input.domain, input.module, 'eventhandler', input.name) + + return { + id, + type: 'EventHandler', + name: input.name, + domain: input.domain, + module: input.module, + subscribedEvents: [...input.subscribedEvents], + sourceLocation: input.sourceLocation, + ...(input.description !== undefined && { description: input.description }), + } + } + + private buildCustomComponent(input: CustomInput): CustomComponent { + validateDomainExists(this.graph.metadata.domains, input.domain) + validateCustomType(this.graph.metadata.customTypes, input.customTypeName) + validateRequiredProperties( + this.graph.metadata.customTypes, + input.customTypeName, + input.metadata, + ) + const id = generateComponentId(input.domain, input.module, 'custom', input.name) + + const component: CustomComponent = { + id, + type: 'Custom', + customTypeName: input.customTypeName, + name: input.name, + domain: input.domain, + module: input.module, + sourceLocation: input.sourceLocation, + ...(input.description !== undefined && { description: input.description }), + ...input.metadata, + } + + return component + } + + private registerComponent(component: T): T { + const result = registerComponent(this.graph, component) + this.replaceGraph(result.graph) + return result.component + } + + private upsertTypedComponent( + incoming: T, + options?: UpsertOptions, + ): { + component: T + created: boolean + } { + const result = upsertComponent(this.graph, incoming, options, this.addWarning) + this.replaceGraph(result.graph) + return { + component: result.component, + created: result.created, + } + } + + private replaceGraph(graph: BuilderGraph): void { + this.graph = graph + this.updateGraph(graph) + } +} diff --git a/packages/riviere-builder/domain-model/src/domain/custom-component-definition.ts b/packages/riviere-builder/domain-model/src/domain/custom-component-definition.ts new file mode 100644 index 000000000..52564faec --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/custom-component-definition.ts @@ -0,0 +1,33 @@ +/** @riviere-role value-object */ +export class CustomComponentDefinition { + declare private readonly brand: 'CustomComponentDefinition' + + private constructor( + readonly customTypeName: string, + readonly metadata: Readonly> | undefined, + ) {} + + static parse(name: string | undefined, properties: readonly string[] | undefined) { + if (name === undefined || name.trim().length === 0) { + return { success: false as const, message: '--custom-type is required for Custom component' } + } + const metadata: Record = {} + for (const property of properties ?? []) { + const separator = property.indexOf(':') + if (separator === -1) { + return { + success: false as const, + message: `Invalid custom property format: ${property}. Expected 'key:value'`, + } + } + metadata[property.slice(0, separator)] = property.slice(separator + 1) + } + return { + success: true as const, + data: new CustomComponentDefinition( + name.trim(), + Object.keys(metadata).length === 0 ? undefined : metadata, + ), + } + } +} diff --git a/packages/riviere-builder/src/features/building/domain/enrichment/builder-enrichment-duplicates.spec.ts b/packages/riviere-builder/domain-model/src/domain/enrichment/builder-enrichment-duplicates.spec.ts similarity index 98% rename from packages/riviere-builder/src/features/building/domain/enrichment/builder-enrichment-duplicates.spec.ts rename to packages/riviere-builder/domain-model/src/domain/enrichment/builder-enrichment-duplicates.spec.ts index 50798a317..1989aa097 100644 --- a/packages/riviere-builder/src/features/building/domain/enrichment/builder-enrichment-duplicates.spec.ts +++ b/packages/riviere-builder/domain-model/src/domain/enrichment/builder-enrichment-duplicates.spec.ts @@ -1,7 +1,5 @@ -import type { RiviereGraph } from '@living-architecture/riviere-schema' -import { - RiviereBuilder, type BuilderOptions -} from '../builder-facade' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' +import { RiviereBuilder } from '../builder-facade' function parseGraph(builder: RiviereBuilder): RiviereGraph { const graph: RiviereGraph = JSON.parse(builder.serialize()) @@ -12,7 +10,7 @@ function findComponent(builder: RiviereBuilder, id: string) { return parseGraph(builder).components.find((c) => c.id === id) } -function createValidOptions(): BuilderOptions { +function createValidOptions() { return { sources: [ { @@ -26,7 +24,7 @@ function createValidOptions(): BuilderOptions { systemType: 'domain', }, }, - } + } as const } function createSourceLocation() { diff --git a/packages/riviere-builder/src/features/building/domain/enrichment/builder-enrichment.spec.ts b/packages/riviere-builder/domain-model/src/domain/enrichment/builder-enrichment.spec.ts similarity index 93% rename from packages/riviere-builder/src/features/building/domain/enrichment/builder-enrichment.spec.ts rename to packages/riviere-builder/domain-model/src/domain/enrichment/builder-enrichment.spec.ts index 2f1884c3c..22e772a73 100644 --- a/packages/riviere-builder/src/features/building/domain/enrichment/builder-enrichment.spec.ts +++ b/packages/riviere-builder/domain-model/src/domain/enrichment/builder-enrichment.spec.ts @@ -1,7 +1,5 @@ -import type { RiviereGraph } from '@living-architecture/riviere-schema' -import { - RiviereBuilder, type BuilderOptions -} from '../builder-facade' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' +import { RiviereBuilder } from '../builder-facade' function parseGraph(builder: RiviereBuilder): RiviereGraph { const graph: RiviereGraph = JSON.parse(builder.serialize()) @@ -12,7 +10,7 @@ function findComponent(builder: RiviereBuilder, id: string) { return parseGraph(builder).components.find((c) => c.id === id) } -function createValidOptions(): BuilderOptions { +function createValidOptions() { return { sources: [ { @@ -26,7 +24,7 @@ function createValidOptions(): BuilderOptions { systemType: 'domain', }, }, - } + } as const } function createSourceLocation() { @@ -80,10 +78,14 @@ describe('RiviereBuilder enrichComponent', () => { sourceLocation: createSourceLocation(), }) - builder.enrichComponent(domainOp.id, {businessRules: ['Customer must have valid payment', 'Inventory must be available'],}) + builder.enrichComponent(domainOp.id, { + businessRules: ['Customer must have valid payment', 'Inventory must be available'], + }) const enriched = findComponent(builder, domainOp.id) - expect(enriched).toMatchObject({businessRules: ['Customer must have valid payment', 'Inventory must be available'],}) + expect(enriched).toMatchObject({ + businessRules: ['Customer must have valid payment', 'Inventory must be available'], + }) }) }) @@ -172,7 +174,9 @@ describe('RiviereBuilder enrichComponent', () => { sourceLocation: createSourceLocation(), }) - builder.enrichComponent(domainOp.id, {behavior: { reads: ['items parameter', 'this.state'] },}) + builder.enrichComponent(domainOp.id, { + behavior: { reads: ['items parameter', 'this.state'] }, + }) const enriched = findComponent(builder, domainOp.id) expect(enriched).toMatchObject({ behavior: { reads: ['items parameter', 'this.state'] } }) @@ -281,7 +285,9 @@ describe('RiviereBuilder enrichComponent', () => { builder.enrichComponent(domainOp.id, { businessRules: ['Inventory must be available'] }) const enriched = findComponent(builder, domainOp.id) - expect(enriched).toMatchObject({businessRules: ['Customer must be authenticated', 'Inventory must be available'],}) + expect(enriched).toMatchObject({ + businessRules: ['Customer must be authenticated', 'Inventory must be available'], + }) }) }) diff --git a/packages/riviere-builder/src/features/building/domain/enrichment/deduplicate-transitions.spec.ts b/packages/riviere-builder/domain-model/src/domain/enrichment/deduplicate-transitions.spec.ts similarity index 100% rename from packages/riviere-builder/src/features/building/domain/enrichment/deduplicate-transitions.spec.ts rename to packages/riviere-builder/domain-model/src/domain/enrichment/deduplicate-transitions.spec.ts diff --git a/packages/riviere-builder/src/features/building/domain/enrichment/deduplicate-transitions.ts b/packages/riviere-builder/domain-model/src/domain/enrichment/deduplicate-transitions.ts similarity index 91% rename from packages/riviere-builder/src/features/building/domain/enrichment/deduplicate-transitions.ts rename to packages/riviere-builder/domain-model/src/domain/enrichment/deduplicate-transitions.ts index 67bacbbb2..e11ea8270 100644 --- a/packages/riviere-builder/src/features/building/domain/enrichment/deduplicate-transitions.ts +++ b/packages/riviere-builder/domain-model/src/domain/enrichment/deduplicate-transitions.ts @@ -1,4 +1,4 @@ -import type { StateTransition } from '@living-architecture/riviere-schema' +import type { StateTransition } from '@living-architecture/riviere-schema-published-language/schema' /** @riviere-role domain-service */ export function deduplicateStateTransitions( diff --git a/packages/riviere-builder/src/features/building/domain/enrichment/enrichment-errors.ts b/packages/riviere-builder/domain-model/src/domain/enrichment/enrichment-errors.ts similarity index 100% rename from packages/riviere-builder/src/features/building/domain/enrichment/enrichment-errors.ts rename to packages/riviere-builder/domain-model/src/domain/enrichment/enrichment-errors.ts diff --git a/packages/riviere-builder/domain-model/src/domain/enrichment/graph-enrichment.ts b/packages/riviere-builder/domain-model/src/domain/enrichment/graph-enrichment.ts new file mode 100644 index 000000000..043d579c2 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/enrichment/graph-enrichment.ts @@ -0,0 +1,69 @@ +import type { DomainOpComponent } from '@living-architecture/riviere-schema-published-language/schema' +import type { BuilderGraph } from '../builder-graph' +import { InvalidEnrichmentTargetError } from './enrichment-errors' +import { createComponentNotFoundError } from '../construction/builder-internals' +import { deduplicateStateTransitions } from './deduplicate-transitions' +import { deduplicateStrings } from '../collection-utils/deduplicate-strings' +import { mergeBehavior } from './merge-behavior' + +type EnrichmentInput = Readonly< + Pick +> + +/** @riviere-role domain-service */ +export class GraphEnrichment { + private graph: BuilderGraph + private readonly updateGraph: (graph: BuilderGraph) => void + + constructor(graph: BuilderGraph, updateGraph: (graph: BuilderGraph) => void) { + this.graph = graph + this.updateGraph = updateGraph + } + + enrichComponent(id: string, enrichment: EnrichmentInput): void { + const componentIndex = this.graph.components.findIndex((component) => component.id === id) + const component = this.graph.components[componentIndex] + if (!component) { + throw createComponentNotFoundError(this.graph.components, id) + } + if (component.type !== 'DomainOp') { + throw new InvalidEnrichmentTargetError(id, component.type) + } + const entityEnriched: DomainOpComponent = { + ...component, + ...(enrichment.entity !== undefined && { entity: enrichment.entity }), + } + const stateEnriched: DomainOpComponent = (() => { + if (enrichment.stateChanges === undefined) { + return entityEnriched + } + const existing = entityEnriched.stateChanges ?? [] + const newItems = deduplicateStateTransitions(existing, enrichment.stateChanges) + return { + ...entityEnriched, + stateChanges: [...existing, ...newItems], + } + })() + const rulesEnriched: DomainOpComponent = (() => { + if (enrichment.businessRules === undefined) { + return stateEnriched + } + const existing = stateEnriched.businessRules ?? [] + const newItems = deduplicateStrings(existing, enrichment.businessRules) + return { + ...stateEnriched, + businessRules: [...existing, ...newItems], + } + })() + const updatedComponent: DomainOpComponent = { + ...rulesEnriched, + ...(enrichment.behavior !== undefined && { + behavior: mergeBehavior(rulesEnriched.behavior, enrichment.behavior), + }), + ...(enrichment.signature !== undefined && { signature: enrichment.signature }), + } + + this.graph = this.graph.withComponentAt(componentIndex, updatedComponent) + this.updateGraph(this.graph) + } +} diff --git a/packages/riviere-builder/src/features/building/domain/enrichment/merge-behavior.spec.ts b/packages/riviere-builder/domain-model/src/domain/enrichment/merge-behavior.spec.ts similarity index 100% rename from packages/riviere-builder/src/features/building/domain/enrichment/merge-behavior.spec.ts rename to packages/riviere-builder/domain-model/src/domain/enrichment/merge-behavior.spec.ts diff --git a/packages/riviere-builder/domain-model/src/domain/enrichment/merge-behavior.ts b/packages/riviere-builder/domain-model/src/domain/enrichment/merge-behavior.ts new file mode 100644 index 000000000..3e722f95b --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/enrichment/merge-behavior.ts @@ -0,0 +1,29 @@ +import type { + DomainOpComponent, + OperationBehavior, +} from '@living-architecture/riviere-schema-published-language/schema' +import { deduplicateStrings } from '../collection-utils/deduplicate-strings' + +function mergeStringArray(existing: string[] | undefined, incoming: string[]): string[] { + const base = existing ?? [] + return [...base, ...deduplicateStrings(base, incoming)] +} + +/** @riviere-role domain-service */ +export function mergeBehavior( + existing: DomainOpComponent['behavior'], + incoming: OperationBehavior, +): OperationBehavior { + const base = existing ?? {} + return { + ...base, + ...(incoming.reads !== undefined && { reads: mergeStringArray(base.reads, incoming.reads) }), + ...(incoming.validates !== undefined && { + validates: mergeStringArray(base.validates, incoming.validates), + }), + ...(incoming.modifies !== undefined && { + modifies: mergeStringArray(base.modifies, incoming.modifies), + }), + ...(incoming.emits !== undefined && { emits: mergeStringArray(base.emits, incoming.emits) }), + } +} diff --git a/packages/riviere-builder/src/features/building/domain/enrichment/upsert-merge.ts b/packages/riviere-builder/domain-model/src/domain/enrichment/upsert-merge.ts similarity index 85% rename from packages/riviere-builder/src/features/building/domain/enrichment/upsert-merge.ts rename to packages/riviere-builder/domain-model/src/domain/enrichment/upsert-merge.ts index 13487de82..f3d8831cd 100644 --- a/packages/riviere-builder/src/features/building/domain/enrichment/upsert-merge.ts +++ b/packages/riviere-builder/domain-model/src/domain/enrichment/upsert-merge.ts @@ -2,12 +2,20 @@ import type { Component, CustomComponent, OperationBehavior, -} from '@living-architecture/riviere-schema' -import type { UpsertOptions } from '../construction/construction-types' -import type { BuilderWarning } from '../inspection/inspection-types' +} from '@living-architecture/riviere-schema-published-language/schema' import { mergeBehavior } from './merge-behavior' type Primitive = string | number | boolean +type AddScalarOverwriteWarning = ( + warning: Readonly<{ + code: 'SCALAR_OVERWRITE' + message: string + componentId: string + field: string + oldValue: Primitive + newValue: Primitive + }>, +) => void const IDENTITY_FIELDS = new Set(['id', 'type', 'name', 'domain', 'module']) const CUSTOM_BASE_FIELDS = new Set([ @@ -25,8 +33,8 @@ const CUSTOM_BASE_FIELDS = new Set([ export function mergeComponentForUpsert( existing: T, incoming: T, - options: UpsertOptions | undefined, - warnings: BuilderWarning[], + options: { readonly noOverwrite?: boolean } | undefined, + addWarning: AddScalarOverwriteWarning, ): T { const merged: T = { ...existing } @@ -35,11 +43,11 @@ export function mergeComponentForUpsert( continue } - mergeTopLevelField(merged, existing.id, field, incomingValue, options, warnings) + mergeTopLevelField(merged, existing.id, field, incomingValue, options, addWarning) } if (isCustomComponent(existing) && isCustomComponent(incoming)) { - const mergedMetadata = mergeCustomMetadata(existing, incoming, options, warnings) + const mergedMetadata = mergeCustomMetadata(existing, incoming, options, addWarning) for (const [key, value] of Object.entries(mergedMetadata)) { setField(merged, key, value) } @@ -53,8 +61,8 @@ function mergeTopLevelField( componentId: string, field: string, incomingValue: unknown, - options: UpsertOptions | undefined, - warnings: BuilderWarning[], + options: { readonly noOverwrite?: boolean } | undefined, + addWarning: AddScalarOverwriteWarning, ): void { if (Array.isArray(incomingValue)) { mergeArrayField(target, field, incomingValue) @@ -72,19 +80,19 @@ function mergeTopLevelField( setField( target, field, - mergeNestedObject(existingRecord, incomingValue, options, warnings, componentId, field), + mergeNestedObject(existingRecord, incomingValue, options, addWarning, componentId, field), ) return } - mergeScalarLikeField(target, field, incomingValue, options, warnings, componentId) + mergeScalarLikeField(target, field, incomingValue, options, addWarning, componentId) } function mergeCustomMetadata( existing: CustomComponent, incoming: CustomComponent, - options: UpsertOptions | undefined, - warnings: BuilderWarning[], + options: { readonly noOverwrite?: boolean } | undefined, + addWarning: AddScalarOverwriteWarning, ): Record { const existingMetadata = extractCustomMetadata(existing) const incomingMetadata = extractCustomMetadata(incoming) @@ -93,7 +101,7 @@ function mergeCustomMetadata( existingMetadata, incomingMetadata, options, - warnings, + addWarning, existing.id, 'metadata', ) @@ -143,8 +151,8 @@ function mergeScalarLikeField( target: object, field: string, incomingValue: unknown, - options: UpsertOptions | undefined, - warnings: BuilderWarning[], + options: { readonly noOverwrite?: boolean } | undefined, + addWarning: AddScalarOverwriteWarning, componentId: string, ): void { const existingValue = getField(target, field) @@ -153,13 +161,13 @@ function mergeScalarLikeField( return } - maybePushScalarOverwriteWarning(warnings, componentId, field, existingValue, incomingValue) + maybeAddScalarOverwriteWarning(addWarning, componentId, field, existingValue, incomingValue) setField(target, field, incomingValue) } -function maybePushScalarOverwriteWarning( - warnings: BuilderWarning[], +function maybeAddScalarOverwriteWarning( + addWarning: AddScalarOverwriteWarning, componentId: string, field: string, existingValue: unknown, @@ -173,7 +181,7 @@ function maybePushScalarOverwriteWarning( return } - warnings.push({ + addWarning({ code: 'SCALAR_OVERWRITE', message: `Scalar field '${field}' on component '${componentId}' overwritten`, componentId, @@ -186,8 +194,8 @@ function maybePushScalarOverwriteWarning( function mergeNestedObject( existing: Record | undefined, incoming: Record, - options: UpsertOptions | undefined, - warnings: BuilderWarning[], + options: { readonly noOverwrite?: boolean } | undefined, + addWarning: AddScalarOverwriteWarning, componentId: string, pathPrefix: string, ): Record { @@ -210,7 +218,7 @@ function mergeNestedObject( existingRecord, incomingValue, options, - warnings, + addWarning, componentId, `${pathPrefix}.${field}`, ) @@ -221,8 +229,8 @@ function mergeNestedObject( continue } - maybePushScalarOverwriteWarning( - warnings, + maybeAddScalarOverwriteWarning( + addWarning, componentId, `${pathPrefix}.${field}`, merged[field], diff --git a/packages/riviere-builder/src/features/building/domain/error-recovery/builder-near-matches.spec.ts b/packages/riviere-builder/domain-model/src/domain/error-recovery/builder-near-matches.spec.ts similarity index 99% rename from packages/riviere-builder/src/features/building/domain/error-recovery/builder-near-matches.spec.ts rename to packages/riviere-builder/domain-model/src/domain/error-recovery/builder-near-matches.spec.ts index 4117675f2..f590bc364 100644 --- a/packages/riviere-builder/src/features/building/domain/error-recovery/builder-near-matches.spec.ts +++ b/packages/riviere-builder/domain-model/src/domain/error-recovery/builder-near-matches.spec.ts @@ -1,6 +1,4 @@ -import { - describe, it, expect -} from 'vitest' +import { describe, it, expect } from 'vitest' import { RiviereBuilder } from '../builder-facade' class TestAssertionError extends Error { diff --git a/packages/riviere-builder/src/features/building/domain/error-recovery/component-suggestion.ts b/packages/riviere-builder/domain-model/src/domain/error-recovery/component-suggestion.ts similarity index 76% rename from packages/riviere-builder/src/features/building/domain/error-recovery/component-suggestion.ts rename to packages/riviere-builder/domain-model/src/domain/error-recovery/component-suggestion.ts index 427cc4a3b..e593ae114 100644 --- a/packages/riviere-builder/src/features/building/domain/error-recovery/component-suggestion.ts +++ b/packages/riviere-builder/domain-model/src/domain/error-recovery/component-suggestion.ts @@ -1,14 +1,29 @@ -import type { - Component, ComponentId -} from '@living-architecture/riviere-schema' +import type { Component } from '@living-architecture/riviere-schema-published-language/schema' +import type { ComponentId } from '@living-architecture/riviere-schema-published-language/component-id' import { ComponentNotFoundError } from '../construction/construction-errors' -import { similarityScore } from '../../../../platform/domain/text-similarity/string-similarity' -import type { - NearMatchMismatch, - NearMatchOptions, - NearMatchQuery, - NearMatchResult, -} from './match-types' +import { similarityScore } from '../text-similarity/string-similarity' +type NearMatchQuery = Readonly<{ + name: string + type?: import('@living-architecture/riviere-schema-published-language/schema').ComponentType + domain?: string +}> + +type NearMatchOptions = Readonly<{ + threshold?: number + limit?: number +}> + +type NearMatchMismatch = Readonly<{ + field: 'type' | 'domain' + expected: string + actual: string +}> + +type NearMatchResult = Readonly<{ + component: Component + score: number + mismatch?: NearMatchMismatch +}> function detectMismatch( query: NearMatchQuery, @@ -58,7 +73,7 @@ function detectMismatch( * ``` */ export function findNearMatches( - components: Component[], + components: readonly Component[], query: NearMatchQuery, options?: NearMatchOptions, ): NearMatchResult[] { @@ -76,7 +91,7 @@ export function findNearMatches( return { component, score, - mismatch, + ...(mismatch !== undefined && { mismatch }), } }) .filter((result) => result.score >= threshold || result.mismatch !== undefined) @@ -102,7 +117,7 @@ export function findNearMatches( * ``` */ export function createSourceNotFoundError( - components: Component[], + components: readonly Component[], id: ComponentId, ): ComponentNotFoundError { const matches = findNearMatches(components, { name: id.name() }, { limit: 3 }) diff --git a/packages/riviere-builder/domain-model/src/domain/error-recovery/near-match.ts b/packages/riviere-builder/domain-model/src/domain/error-recovery/near-match.ts new file mode 100644 index 000000000..5447ae718 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/error-recovery/near-match.ts @@ -0,0 +1,25 @@ +import type { BuilderGraph } from '../builder-graph' +import { findNearMatches } from './component-suggestion' + +/** @riviere-role domain-service */ +export class NearMatch { + private readonly graph: BuilderGraph + + constructor(graph: BuilderGraph) { + this.graph = graph + } + + findNearMatches( + query: Readonly<{ + name: string + type?: import('@living-architecture/riviere-schema-published-language/schema').ComponentType + domain?: string + }>, + options?: Readonly<{ + threshold?: number + limit?: number + }>, + ) { + return findNearMatches(this.graph.components, query, options) + } +} diff --git a/packages/riviere-builder/domain-model/src/domain/http-method.spec.ts b/packages/riviere-builder/domain-model/src/domain/http-method.spec.ts new file mode 100644 index 000000000..317f257f7 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/http-method.spec.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from 'vitest' +import { HttpMethod } from './http-method' + +describe('HttpMethod', () => { + it.each(['GET', 'post', 'Put', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'])('parses %s', (value) => { + const result = HttpMethod.parse(value) + + expect(result.success).toBe(true) + expect(result.success && result.data.value).toBe(value.toUpperCase()) + }) + + it('returns the validation error for an unsupported HTTP method', () => { + const result = HttpMethod.parse('TRACE') + + expect(result.success).toBe(false) + expect(!result.success && result.error.issues).not.toHaveLength(0) + }) +}) diff --git a/packages/riviere-builder/domain-model/src/domain/http-method.ts b/packages/riviere-builder/domain-model/src/domain/http-method.ts new file mode 100644 index 000000000..264d690f2 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/http-method.ts @@ -0,0 +1,24 @@ +import { z } from 'zod' + +const httpMethodSchema = z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']) +type HttpMethodValue = z.infer + +/** @riviere-role value-object */ +export class HttpMethod { + declare private readonly brand: 'HttpMethod' + readonly value: HttpMethodValue + + private constructor(value: HttpMethodValue) { + this.value = value + } + + static parse(value: string) { + const parsed = httpMethodSchema.safeParse(value.toUpperCase()) + return parsed.success + ? { + data: new HttpMethod(parsed.data), + success: true as const, + } + : parsed + } +} diff --git a/packages/riviere-builder/src/features/building/domain/inspection/builder-inspection.spec.ts b/packages/riviere-builder/domain-model/src/domain/inspection/builder-inspection.spec.ts similarity index 95% rename from packages/riviere-builder/src/features/building/domain/inspection/builder-inspection.spec.ts rename to packages/riviere-builder/domain-model/src/domain/inspection/builder-inspection.spec.ts index 66421e1ab..239794fcf 100644 --- a/packages/riviere-builder/src/features/building/domain/inspection/builder-inspection.spec.ts +++ b/packages/riviere-builder/domain-model/src/domain/inspection/builder-inspection.spec.ts @@ -1,12 +1,22 @@ -import { - describe, it, expect -} from 'vitest' +import { describe, it, expect } from 'vitest' import { RiviereBuilder } from '../builder-facade' -import { - createValidOptions, createSourceLocation -} from '../../../../__fixtures__/builder-fixtures' +import { createValidOptions, createSourceLocation } from '../../__fixtures__/builder-fixtures' describe('RiviereBuilder', () => { + describe('query', () => { + it('queries the components currently held by the builder', () => { + const builder = RiviereBuilder.new(createValidOptions()) + const component = builder.addUseCase({ + name: 'Create Order', + domain: 'orders', + module: 'checkout', + sourceLocation: createSourceLocation(), + }) + + expect(builder.query().components()).toStrictEqual([component]) + }) + }) + describe('stats', () => { it('returns zero counts when graph has no components', () => { const builder = RiviereBuilder.new(createValidOptions()) diff --git a/packages/riviere-builder/domain-model/src/domain/inspection/graph-inspection.ts b/packages/riviere-builder/domain-model/src/domain/inspection/graph-inspection.ts new file mode 100644 index 000000000..8e2380624 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/inspection/graph-inspection.ts @@ -0,0 +1,60 @@ +import type { ValidationResult } from '@living-architecture/riviere-schema-published-language/graph-validation' +import type { BuilderGraph } from '../builder-graph' +import { RiviereQuery } from '../query/RiviereQuery' +import { + calculateStats, + findOrphans, + findWarnings, + toRiviereGraph, + validateGraph, +} from './inspection-functions' + +type OperationWarning = + | Readonly<{ + code: 'SCALAR_OVERWRITE' + message: string + componentId: string + field: string + oldValue: string | number | boolean + newValue: string | number | boolean + }> + | Readonly<{ + code: 'DUPLICATE_LINK_SKIPPED' + message: string + source: string + target: string + linkType?: string + targetRepository?: string + targetName: string + }> + +/** @riviere-role domain-service */ +export class GraphInspection { + private readonly graph: BuilderGraph + private readonly operationWarnings: readonly OperationWarning[] + + constructor(graph: BuilderGraph, operationWarnings: readonly OperationWarning[]) { + this.graph = graph + this.operationWarnings = operationWarnings + } + + warnings() { + return [...findWarnings(this.graph), ...this.operationWarnings] + } + + stats() { + return calculateStats(this.graph) + } + + orphans(): string[] { + return findOrphans(this.graph) + } + + validate(): ValidationResult { + return validateGraph(this.graph) + } + + query(): RiviereQuery { + return new RiviereQuery(toRiviereGraph(this.graph)) + } +} diff --git a/packages/riviere-builder/src/features/building/domain/inspection/inspection-functions.ts b/packages/riviere-builder/domain-model/src/domain/inspection/inspection-functions.ts similarity index 76% rename from packages/riviere-builder/src/features/building/domain/inspection/inspection-functions.ts rename to packages/riviere-builder/domain-model/src/domain/inspection/inspection-functions.ts index ef63e1d1d..f5bdd8f3f 100644 --- a/packages/riviere-builder/src/features/building/domain/inspection/inspection-functions.ts +++ b/packages/riviere-builder/domain-model/src/domain/inspection/inspection-functions.ts @@ -7,13 +7,20 @@ import type { RiviereGraph, SourceInfo, RelationshipTypeDefinition, -} from '@living-architecture/riviere-schema' -import { - RiviereQuery, type ValidationResult -} from '@living-architecture/riviere-query' -import type { - BuilderStats, BuilderWarning -} from './inspection-types' +} from '@living-architecture/riviere-schema-published-language/schema' +import { ValidationResult } from '@living-architecture/riviere-schema-published-language/graph-validation' + +type InspectionWarning = + | Readonly<{ + code: 'ORPHAN_COMPONENT' + message: string + componentId: string + }> + | Readonly<{ + code: 'UNUSED_DOMAIN' + message: string + domainName: string + }> interface InspectionGraph { version: string @@ -21,14 +28,14 @@ interface InspectionGraph { name?: string description?: string generated?: string - sources: SourceInfo[] - domains: Record - customTypes: Record - relationshipTypes: Record + sources: readonly SourceInfo[] + domains: Readonly> + customTypes: Readonly> + relationshipTypes: Readonly> } - components: Component[] - links: Link[] - externalLinks: ExternalLink[] + components: readonly Component[] + links: readonly Link[] + externalLinks: readonly ExternalLink[] } /** @@ -74,7 +81,7 @@ export function findOrphans(graph: InspectionGraph): string[] { * // { componentCount: 10, linkCount: 8, domainCount: 2, ... } * ``` */ -export function calculateStats(graph: InspectionGraph): BuilderStats { +export function calculateStats(graph: InspectionGraph) { const components = graph.components return { componentCount: components.length, @@ -109,8 +116,8 @@ export function calculateStats(graph: InspectionGraph): BuilderStats { * // [{ code: 'ORPHAN_COMPONENT', message: '...', componentId: '...' }] * ``` */ -export function findWarnings(graph: InspectionGraph): BuilderWarning[] { - const warnings: BuilderWarning[] = [] +export function findWarnings(graph: InspectionGraph): InspectionWarning[] { + const warnings: InspectionWarning[] = [] for (const id of findOrphans(graph)) { warnings.push({ @@ -160,14 +167,14 @@ export function toRiviereGraph(graph: InspectionGraph): RiviereGraph { metadata: { ...(graph.metadata.name !== undefined && { name: graph.metadata.name }), ...(graph.metadata.description !== undefined && { description: graph.metadata.description }), - sources: graph.metadata.sources, - domains: graph.metadata.domains, - ...(hasCustomTypes && { customTypes: graph.metadata.customTypes }), - ...(hasRelationshipTypes && { relationshipTypes: graph.metadata.relationshipTypes }), + sources: [...graph.metadata.sources], + domains: { ...graph.metadata.domains }, + ...(hasCustomTypes && { customTypes: { ...graph.metadata.customTypes } }), + ...(hasRelationshipTypes && { relationshipTypes: { ...graph.metadata.relationshipTypes } }), }, - components: graph.components, - links: graph.links, - ...(hasExternalLinks && { externalLinks: graph.externalLinks }), + components: [...graph.components], + links: [...graph.links], + ...(hasExternalLinks && { externalLinks: [...graph.externalLinks] }), } } @@ -188,5 +195,5 @@ export function toRiviereGraph(graph: InspectionGraph): RiviereGraph { * ``` */ export function validateGraph(graph: InspectionGraph): ValidationResult { - return new RiviereQuery(toRiviereGraph(graph)).validate() + return ValidationResult.parse(toRiviereGraph(graph)) } diff --git a/packages/riviere-builder/domain-model/src/domain/link-type.spec.ts b/packages/riviere-builder/domain-model/src/domain/link-type.spec.ts new file mode 100644 index 000000000..2cc422f74 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/link-type.spec.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from 'vitest' +import { LinkType } from './link-type' + +describe('LinkType', () => { + it.each(['sync', 'async'])('parses %s', (value) => { + const result = LinkType.parse(value) + + expect(result.success).toBe(true) + expect(result.success && result.data.value).toBe(value) + }) + + it('returns the validation error for an unsupported link type', () => { + const result = LinkType.parse('other') + + expect(result.success).toBe(false) + expect(!result.success && result.error.issues).not.toHaveLength(0) + }) +}) diff --git a/packages/riviere-builder/domain-model/src/domain/link-type.ts b/packages/riviere-builder/domain-model/src/domain/link-type.ts new file mode 100644 index 000000000..a8cbdd1df --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/link-type.ts @@ -0,0 +1,24 @@ +import { z } from 'zod' + +const linkTypeSchema = z.enum(['sync', 'async']) +type LinkTypeValue = z.infer + +/** @riviere-role value-object */ +export class LinkType { + declare private readonly brand: 'LinkType' + readonly value: LinkTypeValue + + private constructor(value: LinkTypeValue) { + this.value = value + } + + static parse(value: string) { + const parsed = linkTypeSchema.safeParse(value) + return parsed.success + ? { + data: new LinkType(parsed.data), + success: true as const, + } + : parsed + } +} diff --git a/packages/riviere-builder/src/features/building/domain/linking/builder-link-occurrences.spec.ts b/packages/riviere-builder/domain-model/src/domain/linking/builder-link-occurrences.spec.ts similarity index 97% rename from packages/riviere-builder/src/features/building/domain/linking/builder-link-occurrences.spec.ts rename to packages/riviere-builder/domain-model/src/domain/linking/builder-link-occurrences.spec.ts index bf2ee7176..9163db0d3 100644 --- a/packages/riviere-builder/src/features/building/domain/linking/builder-link-occurrences.spec.ts +++ b/packages/riviere-builder/domain-model/src/domain/linking/builder-link-occurrences.spec.ts @@ -1,12 +1,8 @@ -import { - describe, expect, it -} from 'vitest' -import { - RiviereBuilder, type BuilderOptions -} from '../builder-facade' +import { describe, expect, it } from 'vitest' +import { RiviereBuilder } from '../builder-facade' function createBuilder(): RiviereBuilder { - const options: BuilderOptions = { + const options = { sources: [{ repository: 'test/repo' }], domains: { orders: { @@ -14,7 +10,7 @@ function createBuilder(): RiviereBuilder { systemType: 'domain', }, }, - } + } as const return RiviereBuilder.new(options) } @@ -34,9 +30,9 @@ function replaceFirstLinkId(graph: ReturnType, id: stri graph.links = graph.links.map((link, index) => index === 0 ? { - ...link, - id, - } + ...link, + id, + } : link, ) } diff --git a/packages/riviere-builder/src/features/building/domain/linking/builder-links.spec.ts b/packages/riviere-builder/domain-model/src/domain/linking/builder-links.spec.ts similarity index 95% rename from packages/riviere-builder/src/features/building/domain/linking/builder-links.spec.ts rename to packages/riviere-builder/domain-model/src/domain/linking/builder-links.spec.ts index b8c0802ad..56db0909e 100644 --- a/packages/riviere-builder/src/features/building/domain/linking/builder-links.spec.ts +++ b/packages/riviere-builder/domain-model/src/domain/linking/builder-links.spec.ts @@ -1,11 +1,7 @@ -import { - describe, it, expect -} from 'vitest' -import { - RiviereBuilder, type BuilderOptions -} from '../builder-facade' - -function createValidOptions(): BuilderOptions { +import { describe, it, expect } from 'vitest' +import { RiviereBuilder } from '../builder-facade' + +function createValidOptions() { return { sources: [ { @@ -19,7 +15,7 @@ function createValidOptions(): BuilderOptions { systemType: 'domain', }, }, - } + } as const } describe('RiviereBuilder', () => { @@ -68,6 +64,17 @@ describe('RiviereBuilder', () => { ).toThrow("Source component 'nonexistent:module:usecase:foo' not found") }) + it('reports an invalid source component ID without attempting suggestions', () => { + const builder = RiviereBuilder.new(createValidOptions()) + + expect(() => + builder.link({ + from: 'not-a-component-id', + to: 'any:target:id', + }), + ).toThrow("Source component 'not-a-component-id' not found") + }) + it('includes near-match suggestions when source has typo', () => { const builder = RiviereBuilder.new(createValidOptions()) diff --git a/packages/riviere-builder/domain-model/src/domain/linking/graph-linking.ts b/packages/riviere-builder/domain-model/src/domain/linking/graph-linking.ts new file mode 100644 index 000000000..fe0e11452 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/linking/graph-linking.ts @@ -0,0 +1,141 @@ +import type { + ExternalLink, + Link, +} from '@living-architecture/riviere-schema-published-language/schema' +import { LinkId } from '@living-architecture/riviere-schema-published-language/link-id' +import type { BuilderGraph } from '../builder-graph' +import { createComponentNotFoundError } from '../construction/builder-internals' +import { + DuplicateLinkError, + RelationshipTypeNotFoundError, +} from '../construction/construction-errors' + +type LinkInput = Readonly<{ + from: Link['source'] + to: Link['target'] + type?: Link['type'] + relationshipType?: Link['relationshipType'] + condition?: Link['condition'] + sourceLocation?: Link['sourceLocation'] +}> + +type ExternalLinkInput = Readonly<{ + from: ExternalLink['source'] + target: ExternalLink['target'] + type?: ExternalLink['type'] + description?: ExternalLink['description'] + sourceLocation?: ExternalLink['sourceLocation'] + metadata?: Readonly> +}> + +type AddDuplicateLinkWarning = ( + warning: Readonly<{ + code: 'DUPLICATE_LINK_SKIPPED' + message: string + source: string + target: string + linkType?: string + targetRepository?: string + targetName: string + }>, +) => void + +/** @riviere-role domain-service */ +export class GraphLinking { + private graph: BuilderGraph + private readonly addWarning: AddDuplicateLinkWarning + private readonly updateGraph: (graph: BuilderGraph) => void + + constructor( + graph: BuilderGraph, + addWarning: AddDuplicateLinkWarning, + updateGraph: (graph: BuilderGraph) => void, + ) { + this.graph = graph + this.addWarning = addWarning + this.updateGraph = updateGraph + } + + link(input: LinkInput): Link { + const sourceExists = this.graph.components.some((c) => c.id === input.from) + if (!sourceExists) { + throw createComponentNotFoundError(this.graph.components, input.from) + } + + if ( + input.relationshipType !== undefined && + !Object.hasOwn(this.graph.metadata.relationshipTypes, input.relationshipType) + ) { + throw new RelationshipTypeNotFoundError( + input.relationshipType, + Object.keys(this.graph.metadata.relationshipTypes), + ) + } + + const id = LinkId.parseFromLink({ + source: input.from, + target: input.to, + ...(input.sourceLocation !== undefined && { sourceLocation: input.sourceLocation }), + }).toString() + if ( + this.graph.links.some( + (link) => link.id === id || LinkId.parseFromLink(link).toString() === id, + ) + ) { + throw new DuplicateLinkError(id) + } + + const link: Link = { + id, + source: input.from, + target: input.to, + ...(input.type !== undefined && { type: input.type }), + ...(input.relationshipType !== undefined && { relationshipType: input.relationshipType }), + ...(input.condition !== undefined && { condition: input.condition }), + ...(input.sourceLocation !== undefined && { sourceLocation: input.sourceLocation }), + } + this.graph = this.graph.withLink(link) + this.updateGraph(this.graph) + return link + } + + linkExternal(input: ExternalLinkInput): ExternalLink { + const sourceExists = this.graph.components.some((c) => c.id === input.from) + if (!sourceExists) { + throw createComponentNotFoundError(this.graph.components, input.from) + } + + const duplicate = this.graph.externalLinks.find( + (link) => + link.source === input.from && + link.target.repository === input.target.repository && + link.target.name === input.target.name && + link.type === input.type, + ) + + if (duplicate) { + this.addWarning({ + code: 'DUPLICATE_LINK_SKIPPED', + message: `Duplicate external link '${input.from}' -> '${input.target.name}' (${input.type ?? 'unspecified'}) skipped`, + source: input.from, + target: input.target.name, + ...(input.type !== undefined && { linkType: input.type }), + ...(input.target.repository !== undefined && { targetRepository: input.target.repository }), + targetName: input.target.name, + }) + + return duplicate + } + + const externalLink: ExternalLink = { + source: input.from, + target: input.target, + ...(input.type !== undefined && { type: input.type }), + ...(input.description !== undefined && { description: input.description }), + ...(input.sourceLocation !== undefined && { sourceLocation: input.sourceLocation }), + } + this.graph = this.graph.withExternalLink(externalLink) + this.updateGraph(this.graph) + return externalLink + } +} diff --git a/packages/riviere-query/src/features/querying/queries/RiviereQuery.spec.ts b/packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.spec.ts similarity index 96% rename from packages/riviere-query/src/features/querying/queries/RiviereQuery.spec.ts rename to packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.spec.ts index d6d0d3b20..f25de206c 100644 --- a/packages/riviere-query/src/features/querying/queries/RiviereQuery.spec.ts +++ b/packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.spec.ts @@ -1,13 +1,12 @@ import { - RiviereQuery, parseComponentId -} from './RiviereQuery' -import { - createMinimalValidGraph, createAPIComponent, - createEventHandlerComponent, createCustomComponent, + createEventHandlerComponent, + createMinimalValidGraph, createUseCaseComponent, -} from '../../../platform/__fixtures__/riviere-graph-fixtures' +} from './__fixtures__/riviere-graph-fixtures' +import { RiviereQuery } from './RiviereQuery' +import { ComponentId } from './component-id' describe('RiviereQuery', () => { describe('constructor', () => { @@ -85,14 +84,14 @@ describe('RiviereQuery', () => { const query = new RiviereQuery(graph) - expect(query.detectOrphans()).toStrictEqual([]) + expect(query.detectOrphans().map((id) => id.value)).toStrictEqual([]) }) it('returns orphan IDs when components have no links', () => { const graph = createMinimalValidGraph() const query = new RiviereQuery(graph) - expect(query.detectOrphans()).toStrictEqual(['test:mod:ui:page']) + expect(query.detectOrphans().map((id) => id.value)).toStrictEqual(['test:mod:ui:page']) }) it('considers both source and target links as connected', () => { @@ -118,7 +117,7 @@ describe('RiviereQuery', () => { const query = new RiviereQuery(graph) - expect(query.detectOrphans()).toStrictEqual(['test:mod:ui:page']) + expect(query.detectOrphans().map((id) => id.value)).toStrictEqual(['test:mod:ui:page']) }) }) @@ -186,7 +185,7 @@ describe('RiviereQuery', () => { it('returns component when ID exists', () => { const query = new RiviereQuery(createMinimalValidGraph()) - const result = query.componentById(parseComponentId('test:mod:ui:page')) + const result = query.componentById(ComponentId.parse('test:mod:ui:page')) expect(result?.id).toBe('test:mod:ui:page') }) @@ -194,7 +193,7 @@ describe('RiviereQuery', () => { it('returns undefined when ID does not exist', () => { const query = new RiviereQuery(createMinimalValidGraph()) - expect(query.componentById(parseComponentId('nonexistent:id'))).toBeUndefined() + expect(query.componentById(ComponentId.parse('nonexistent:id'))).toBeUndefined() }) }) diff --git a/packages/riviere-query/src/features/querying/queries/RiviereQuery.ts b/packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.ts similarity index 81% rename from packages/riviere-query/src/features/querying/queries/RiviereQuery.ts rename to packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.ts index 18c475cae..3781806df 100644 --- a/packages/riviere-query/src/features/querying/queries/RiviereQuery.ts +++ b/packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.ts @@ -1,96 +1,63 @@ import type { - RiviereGraph, Component, - Link, ComponentType, DomainOpComponent, ExternalLink, -} from '@living-architecture/riviere-schema' -import type { - Entity, EntityTransition, PublishedEvent, EventHandlerInfo -} from './event-types' -import type { - State, - ComponentId, - LinkId, - ValidationResult, - GraphDiff, - Domain, - Flow, - SearchWithFlowResult, - CrossDomainLink, - DomainConnection, - GraphStats, - ExternalDomain, -} from './domain-types' -import { parseRiviereGraph } from '@living-architecture/riviere-schema' + Link, + RiviereGraph, +} from '@living-architecture/riviere-schema-published-language/schema' +import { parseRiviereGraph } from '@living-architecture/riviere-schema-published-language/validation' +import type { GraphDiff } from './graph-diff' +import type { ComponentDepths } from './component-depths' +import type { ComponentId } from './component-id' import { - findComponent, + componentsInDomain as filterByDomain, + componentsByType as filterByType, findAllComponents, + findComponent, componentById as lookupComponentById, searchComponents, - componentsInDomain as filterByDomain, - componentsByType as filterByType, } from './component-queries' +import type { CrossDomainLink } from './cross-domain-link' +import { queryCrossDomainLinks, queryDomainConnections } from './cross-domain-queries' +import { queryNodeDepths } from './depth-queries' +import type { Domain } from './domain' +import type { DomainConnection } from './domain-connection' import { - queryDomains, + businessRulesForEntity, operationsForEntity, + queryDomains, queryEntities, - businessRulesForEntity, - transitionsForEntity, statesForEntity, + transitionsForEntity, } from './domain-queries' +import type { Entity } from './entity' +import type { EntityTransition } from './entity-transition' +import type { EventHandlerInfo } from './event-handler-info' +import { queryEventHandlers, queryPublishedEvents } from './event-queries' +import type { ExternalDomain } from './external-domain' import { queryExternalDomains } from './external-system-queries' -import { - findEntryPoints, - traceFlowFrom, - queryFlows, - searchWithFlowContext, - type SearchWithFlowOptions, -} from './flow-queries' -import { - queryCrossDomainLinks, queryDomainConnections -} from './cross-domain-queries' -import { - queryPublishedEvents, queryEventHandlers -} from './event-queries' -import { - validateGraph, detectOrphanComponents -} from './graph-validation' +import type { Flow } from './flow' +import { findEntryPoints, queryFlows, searchWithFlowContext, traceFlowFrom } from './flow-queries' import { diffGraphs } from './graph-diff' +import type { GraphStats } from './graph-stats' +import { detectOrphanComponents } from './graph-validation' +import { ValidationResult } from '@living-architecture/riviere-schema-published-language/graph-validation' +import type { LinkId } from './link-id' +import type { PublishedEvent } from './published-event' +import type { SearchWithFlowOptions } from './search-with-flow-options' +import type { SearchWithFlowResult } from './search-with-flow-result' +import type { State } from './state' import { queryStats } from './stats-queries' -import { queryNodeDepths } from './depth-queries' - -export type { - Entity, EntityTransition -} from './event-types' -export type { - ComponentId, - LinkId, - ValidationErrorCode, - ValidationError, - ValidationResult, - Domain, - ComponentCounts, - ComponentModification, - DiffStats, - GraphDiff, - Flow, - FlowStep, - LinkType, - SearchWithFlowResult, - CrossDomainLink, - DomainConnection, - GraphStats, - ExternalDomain, -} from './domain-types' -export type { SearchWithFlowOptions } from './flow-queries' -export { parseComponentId } from './domain-types' +import { InvalidRiviereGraphError } from './errors' export { ComponentNotFoundError } from './errors' function assertValidGraph(graph: unknown): asserts graph is RiviereGraph { - parseRiviereGraph(graph) + const result = parseRiviereGraph(graph) + if (!result.success) { + throw new InvalidRiviereGraphError(result.issues) + } } /** @@ -101,7 +68,7 @@ function assertValidGraph(graph: unknown): asserts graph is RiviereGraph { * * @example * ```typescript - * import { RiviereQuery } from '@living-architecture/riviere-query' + * import { RiviereQuery } from '@living-architecture/riviere-builder-domain-model/query' * * // From JSON * const query = RiviereQuery.fromJSON(graphData) @@ -114,10 +81,10 @@ function assertValidGraph(graph: unknown): asserts graph is RiviereGraph { * const flow = query.traceFlow('orders:checkout:api:post-orders') * ``` * - * @riviere-role query-model + * @riviere-role domain-service */ export class RiviereQuery { - private readonly graph: RiviereGraph + private readonly graphSnapshot: RiviereGraph /** * Creates a new RiviereQuery instance. @@ -133,7 +100,7 @@ export class RiviereQuery { */ constructor(graph: RiviereGraph) { assertValidGraph(graph) - this.graph = graph + this.graphSnapshot = graph } /** @@ -166,7 +133,7 @@ export class RiviereQuery { * ``` */ components(): Component[] { - return this.graph.components + return this.graphSnapshot.components } /** @@ -181,7 +148,7 @@ export class RiviereQuery { * ``` */ links(): Link[] { - return this.graph.links + return this.graphSnapshot.links } /** @@ -200,7 +167,7 @@ export class RiviereQuery { * ``` */ validate(): ValidationResult { - return validateGraph(this.graph) + return ValidationResult.parse(this.graphSnapshot) } /** @@ -217,7 +184,7 @@ export class RiviereQuery { * ``` */ detectOrphans(): ComponentId[] { - return detectOrphanComponents(this.graph) + return detectOrphanComponents(this.graphSnapshot) } /** @@ -232,7 +199,7 @@ export class RiviereQuery { * ``` */ find(predicate: (component: Component) => boolean): Component | undefined { - return findComponent(this.graph, predicate) + return findComponent(this.graphSnapshot, predicate) } /** @@ -249,7 +216,7 @@ export class RiviereQuery { * ``` */ findAll(predicate: (component: Component) => boolean): Component[] { - return findAllComponents(this.graph, predicate) + return findAllComponents(this.graphSnapshot, predicate) } /** @@ -264,7 +231,7 @@ export class RiviereQuery { * ``` */ componentById(id: ComponentId): Component | undefined { - return lookupComponentById(this.graph, id) + return lookupComponentById(this.graphSnapshot, id.value) } /** @@ -282,7 +249,7 @@ export class RiviereQuery { * ``` */ search(query: string): Component[] { - return searchComponents(this.graph, query) + return searchComponents(this.graphSnapshot, query) } /** @@ -297,7 +264,7 @@ export class RiviereQuery { * ``` */ componentsInDomain(domainName: string): Component[] { - return filterByDomain(this.graph, domainName) + return filterByDomain(this.graphSnapshot, domainName) } /** @@ -313,7 +280,7 @@ export class RiviereQuery { * ``` */ componentsByType(type: ComponentType): Component[] { - return filterByType(this.graph, type) + return filterByType(this.graphSnapshot, type) } /** @@ -330,7 +297,7 @@ export class RiviereQuery { * ``` */ domains(): Domain[] { - return queryDomains(this.graph) + return queryDomains(this.graphSnapshot) } /** @@ -345,7 +312,7 @@ export class RiviereQuery { * ``` */ operationsFor(entityName: string): DomainOpComponent[] { - return operationsForEntity(this.graph, entityName) + return operationsForEntity(this.graphSnapshot, entityName) } /** @@ -365,7 +332,7 @@ export class RiviereQuery { * ``` */ entities(domainName?: string): Entity[] { - return queryEntities(this.graph, domainName) + return queryEntities(this.graphSnapshot, domainName) } /** @@ -380,7 +347,7 @@ export class RiviereQuery { * ``` */ businessRulesFor(entityName: string): string[] { - return businessRulesForEntity(this.graph, entityName) + return businessRulesForEntity(this.graphSnapshot, entityName) } /** @@ -395,7 +362,7 @@ export class RiviereQuery { * ``` */ transitionsFor(entityName: string): EntityTransition[] { - return transitionsForEntity(this.graph, entityName) + return transitionsForEntity(this.graphSnapshot, entityName) } /** @@ -413,7 +380,7 @@ export class RiviereQuery { * ``` */ statesFor(entityName: string): State[] { - return statesForEntity(this.graph, entityName) + return statesForEntity(this.graphSnapshot, entityName) } /** @@ -430,7 +397,7 @@ export class RiviereQuery { * ``` */ entryPoints(): Component[] { - return findEntryPoints(this.graph) + return findEntryPoints(this.graphSnapshot) } /** @@ -452,7 +419,7 @@ export class RiviereQuery { componentIds: ComponentId[] linkIds: LinkId[] } { - return traceFlowFrom(this.graph, startComponentId) + return traceFlowFrom(this.graphSnapshot, startComponentId) } /** @@ -472,7 +439,7 @@ export class RiviereQuery { * ``` */ diff(other: RiviereGraph): GraphDiff { - return diffGraphs(this.graph, other) + return diffGraphs(this.graphSnapshot, other) } /** @@ -492,7 +459,7 @@ export class RiviereQuery { * ``` */ publishedEvents(domainName?: string): PublishedEvent[] { - return queryPublishedEvents(this.graph, domainName) + return queryPublishedEvents(this.graphSnapshot, domainName) } /** @@ -508,7 +475,7 @@ export class RiviereQuery { * ``` */ eventHandlers(eventName?: string): EventHandlerInfo[] { - return queryEventHandlers(this.graph, eventName) + return queryEventHandlers(this.graphSnapshot, eventName) } /** @@ -532,7 +499,7 @@ export class RiviereQuery { * ``` */ flows(): Flow[] { - return queryFlows(this.graph) + return queryFlows(this.graphSnapshot) } /** @@ -552,7 +519,7 @@ export class RiviereQuery { * ``` */ searchWithFlow(query: string, options: SearchWithFlowOptions): SearchWithFlowResult { - return searchWithFlowContext(this.graph, query, options) + return searchWithFlowContext(this.graphSnapshot, query, options) } /** @@ -567,7 +534,7 @@ export class RiviereQuery { * ``` */ crossDomainLinks(domainName: string): CrossDomainLink[] { - return queryCrossDomainLinks(this.graph, domainName) + return queryCrossDomainLinks(this.graphSnapshot, domainName) } /** @@ -587,7 +554,7 @@ export class RiviereQuery { * ``` */ domainConnections(domainName: string): DomainConnection[] { - return queryDomainConnections(this.graph, domainName) + return queryDomainConnections(this.graphSnapshot, domainName) } /** @@ -604,7 +571,7 @@ export class RiviereQuery { * ``` */ stats(): GraphStats { - return queryStats(this.graph) + return queryStats(this.graphSnapshot) } /** @@ -622,8 +589,8 @@ export class RiviereQuery { * } * ``` */ - nodeDepths(): Map { - return queryNodeDepths(this.graph) + nodeDepths(): ComponentDepths { + return queryNodeDepths(this.graphSnapshot) } /** @@ -643,7 +610,7 @@ export class RiviereQuery { * ``` */ externalLinks(): ExternalLink[] { - return this.graph.externalLinks ?? [] + return this.graphSnapshot.externalLinks ?? [] } /** @@ -663,6 +630,6 @@ export class RiviereQuery { * ``` */ externalDomains(): ExternalDomain[] { - return queryExternalDomains(this.graph) + return queryExternalDomains(this.graphSnapshot) } } diff --git a/packages/riviere-query/src/features/querying/queries/RiviereQuery.validation.spec.ts b/packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.validation.spec.ts similarity index 96% rename from packages/riviere-query/src/features/querying/queries/RiviereQuery.validation.spec.ts rename to packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.validation.spec.ts index bb8791c6f..7b184dc18 100644 --- a/packages/riviere-query/src/features/querying/queries/RiviereQuery.validation.spec.ts +++ b/packages/riviere-builder/domain-model/src/domain/query/RiviereQuery.validation.spec.ts @@ -1,5 +1,5 @@ +import { createMinimalValidGraph } from './__fixtures__/riviere-graph-fixtures' import { RiviereQuery } from './RiviereQuery' -import { createMinimalValidGraph } from '../../../platform/__fixtures__/riviere-graph-fixtures' describe('RiviereQuery validate()', () => { it('returns valid=true for a valid minimal graph', () => { @@ -95,7 +95,9 @@ describe('RiviereQuery validate()', () => { it('returns valid when Custom type has no requiredProperties', () => { const graph = createMinimalValidGraph() - graph.metadata.customTypes = {SimpleJob: { description: 'A simple job with no required properties' },} + graph.metadata.customTypes = { + SimpleJob: { description: 'A simple job with no required properties' }, + } graph.components.push({ id: 'test:mod:custom:simplejob', type: 'Custom', @@ -128,7 +130,7 @@ describe('RiviereQuery validate()', () => { const result = new RiviereQuery(graph).validate() - expect(result).toStrictEqual({ + expect(JSON.parse(JSON.stringify(result))).toStrictEqual({ valid: true, errors: [], }) diff --git a/packages/riviere-query/src/platform/__fixtures__/riviere-graph-fixtures.ts b/packages/riviere-builder/domain-model/src/domain/query/__fixtures__/riviere-graph-fixtures.ts similarity index 97% rename from packages/riviere-query/src/platform/__fixtures__/riviere-graph-fixtures.ts rename to packages/riviere-builder/domain-model/src/domain/query/__fixtures__/riviere-graph-fixtures.ts index efbd6f655..f40b2ea78 100644 --- a/packages/riviere-query/src/platform/__fixtures__/riviere-graph-fixtures.ts +++ b/packages/riviere-builder/domain-model/src/domain/query/__fixtures__/riviere-graph-fixtures.ts @@ -1,13 +1,13 @@ import type { - RiviereGraph, APIComponent, - EventComponent, - EventHandlerComponent, CustomComponent, - UseCaseComponent, DomainOpComponent, + EventComponent, + EventHandlerComponent, + RiviereGraph, SourceLocation, -} from '@living-architecture/riviere-schema' + UseCaseComponent, +} from '@living-architecture/riviere-schema-published-language/schema' class TestAssertionError extends Error { constructor(message: string) { diff --git a/packages/riviere-query/src/features/querying/queries/compare-by-code-point.spec.ts b/packages/riviere-builder/domain-model/src/domain/query/compare-by-code-point.spec.ts similarity index 95% rename from packages/riviere-query/src/features/querying/queries/compare-by-code-point.spec.ts rename to packages/riviere-builder/domain-model/src/domain/query/compare-by-code-point.spec.ts index 5672699b8..71f30db30 100644 --- a/packages/riviere-query/src/features/querying/queries/compare-by-code-point.spec.ts +++ b/packages/riviere-builder/domain-model/src/domain/query/compare-by-code-point.spec.ts @@ -1,6 +1,4 @@ -import { - describe, it, expect -} from 'vitest' +import { describe, expect, it } from 'vitest' import { compareByCodePoint } from './compare-by-code-point' describe('compareByCodePoint', () => { diff --git a/packages/riviere-query/src/features/querying/queries/compare-by-code-point.ts b/packages/riviere-builder/domain-model/src/domain/query/compare-by-code-point.ts similarity index 95% rename from packages/riviere-query/src/features/querying/queries/compare-by-code-point.ts rename to packages/riviere-builder/domain-model/src/domain/query/compare-by-code-point.ts index d7819ed1e..01aadfba5 100644 --- a/packages/riviere-query/src/features/querying/queries/compare-by-code-point.ts +++ b/packages/riviere-builder/domain-model/src/domain/query/compare-by-code-point.ts @@ -1,4 +1,4 @@ -/** @riviere-role query-model */ +/** @riviere-role domain-service */ export function compareByCodePoint(a: string, b: string): number { const leftCodePoints = Array.from(a, toCodePoint) const rightCodePoints = Array.from(b, toCodePoint) diff --git a/packages/riviere-builder/domain-model/src/domain/query/component-counts.ts b/packages/riviere-builder/domain-model/src/domain/query/component-counts.ts new file mode 100644 index 000000000..28a3168f1 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/component-counts.ts @@ -0,0 +1,45 @@ +/** @riviere-role value-object */ +export class ComponentCounts { + declare private readonly brand: 'ComponentCounts' + readonly UI: number + readonly API: number + readonly UseCase: number + readonly DomainOp: number + readonly Event: number + readonly EventHandler: number + readonly Custom: number + readonly total: number + + private constructor(input: { + readonly UI: number + readonly API: number + readonly UseCase: number + readonly DomainOp: number + readonly Event: number + readonly EventHandler: number + readonly Custom: number + readonly total: number + }) { + this.UI = input.UI + this.API = input.API + this.UseCase = input.UseCase + this.DomainOp = input.DomainOp + this.Event = input.Event + this.EventHandler = input.EventHandler + this.Custom = input.Custom + this.total = input.total + } + + static parse(input: { + readonly UI: number + readonly API: number + readonly UseCase: number + readonly DomainOp: number + readonly Event: number + readonly EventHandler: number + readonly Custom: number + readonly total: number + }): ComponentCounts { + return new ComponentCounts(input) + } +} diff --git a/packages/riviere-builder/domain-model/src/domain/query/component-depths.ts b/packages/riviere-builder/domain-model/src/domain/query/component-depths.ts new file mode 100644 index 000000000..89bc1db71 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/component-depths.ts @@ -0,0 +1,27 @@ +import type { ComponentId } from './component-id' + +/** @riviere-role value-object */ +export class ComponentDepths { + declare private readonly brand: 'ComponentDepths' + private readonly depths: ReadonlyMap + + private constructor(depths: ReadonlyMap) { + this.depths = new Map(depths) + } + + static parse(depths: ReadonlyMap): ComponentDepths { + return new ComponentDepths(depths) + } + + get size(): number { + return this.depths.size + } + + get(componentId: ComponentId): number | undefined { + return this.depths.get(componentId.value) + } + + has(componentId: ComponentId): boolean { + return this.depths.has(componentId.value) + } +} diff --git a/packages/riviere-builder/domain-model/src/domain/query/component-id.ts b/packages/riviere-builder/domain-model/src/domain/query/component-id.ts new file mode 100644 index 000000000..1bfce061a --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/component-id.ts @@ -0,0 +1,22 @@ +import { z } from 'zod' + +const schema = z.string() + +/** @riviere-role value-object */ +export class ComponentId { + declare private readonly brand: 'ComponentId' + + private constructor(readonly value: string) {} + + static parse(value: string): ComponentId { + return new ComponentId(schema.parse(value)) + } + + localeCompare(other: ComponentId): number { + return this.value.localeCompare(other.value) + } + + toJSON(): string { + return this.value + } +} diff --git a/packages/riviere-builder/domain-model/src/domain/query/component-modification.ts b/packages/riviere-builder/domain-model/src/domain/query/component-modification.ts new file mode 100644 index 000000000..ba5b0a393 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/component-modification.ts @@ -0,0 +1,32 @@ +import type { Component } from '@living-architecture/riviere-schema-published-language/schema' +import { ComponentId } from './component-id' + +/** @riviere-role value-object */ +export class ComponentModification { + declare private readonly brand: 'ComponentModification' + readonly id: ComponentId + readonly before: Component + readonly after: Component + readonly changedFields: string[] + + private constructor(input: { + readonly id: ComponentId + readonly before: Component + readonly after: Component + readonly changedFields: string[] + }) { + this.id = input.id + this.before = input.before + this.after = input.after + this.changedFields = input.changedFields + } + + static parse(input: { + readonly id: ComponentId + readonly before: Component + readonly after: Component + readonly changedFields: string[] + }): ComponentModification { + return new ComponentModification(input) + } +} diff --git a/packages/riviere-query/src/features/querying/queries/component-queries.ts b/packages/riviere-builder/domain-model/src/domain/query/component-queries.ts similarity index 78% rename from packages/riviere-query/src/features/querying/queries/component-queries.ts rename to packages/riviere-builder/domain-model/src/domain/query/component-queries.ts index b0683037c..feec2a4ba 100644 --- a/packages/riviere-query/src/features/querying/queries/component-queries.ts +++ b/packages/riviere-builder/domain-model/src/domain/query/component-queries.ts @@ -1,8 +1,10 @@ import type { - RiviereGraph, Component, ComponentType -} from '@living-architecture/riviere-schema' + Component, + ComponentType, + RiviereGraph, +} from '@living-architecture/riviere-schema-published-language/schema' -/** @riviere-role query-model */ +/** @riviere-role domain-service */ export function findComponent( graph: RiviereGraph, predicate: (component: Component) => boolean, @@ -10,7 +12,7 @@ export function findComponent( return graph.components.find(predicate) } -/** @riviere-role query-model */ +/** @riviere-role domain-service */ export function findAllComponents( graph: RiviereGraph, predicate: (component: Component) => boolean, @@ -18,12 +20,12 @@ export function findAllComponents( return graph.components.filter(predicate) } -/** @riviere-role query-model */ +/** @riviere-role domain-service */ export function componentById(graph: RiviereGraph, id: string): Component | undefined { return findComponent(graph, (c) => c.id === id) } -/** @riviere-role query-model */ +/** @riviere-role domain-service */ export function searchComponents(graph: RiviereGraph, query: string): Component[] { if (query === '') { return [] @@ -38,12 +40,12 @@ export function searchComponents(graph: RiviereGraph, query: string): Component[ ) } -/** @riviere-role query-model */ +/** @riviere-role domain-service */ export function componentsInDomain(graph: RiviereGraph, domainName: string): Component[] { return findAllComponents(graph, (c) => c.domain === domainName) } -/** @riviere-role query-model */ +/** @riviere-role domain-service */ export function componentsByType(graph: RiviereGraph, type: ComponentType): Component[] { return findAllComponents(graph, (c) => c.type === type) } diff --git a/packages/riviere-builder/domain-model/src/domain/query/cross-domain-link.ts b/packages/riviere-builder/domain-model/src/domain/query/cross-domain-link.ts new file mode 100644 index 000000000..798dc32d7 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/cross-domain-link.ts @@ -0,0 +1,25 @@ +import { DomainName } from './domain-name' + +type CrossDomainLinkType = 'sync' | 'async' | undefined + +/** @riviere-role value-object */ +export class CrossDomainLink { + declare private readonly brand: 'CrossDomainLink' + readonly targetDomain: DomainName + readonly linkType: CrossDomainLinkType + + private constructor(input: { + readonly targetDomain: DomainName + readonly linkType: CrossDomainLinkType + }) { + this.targetDomain = input.targetDomain + this.linkType = input.linkType + } + + static parse(input: { + readonly targetDomain: DomainName + readonly linkType: CrossDomainLinkType + }): CrossDomainLink { + return new CrossDomainLink(input) + } +} diff --git a/packages/riviere-query/src/features/querying/queries/cross-domain-links.spec.ts b/packages/riviere-builder/domain-model/src/domain/query/cross-domain-links.spec.ts similarity index 92% rename from packages/riviere-query/src/features/querying/queries/cross-domain-links.spec.ts rename to packages/riviere-builder/domain-model/src/domain/query/cross-domain-links.spec.ts index 52ce81547..5b50b51f1 100644 --- a/packages/riviere-query/src/features/querying/queries/cross-domain-links.spec.ts +++ b/packages/riviere-builder/domain-model/src/domain/query/cross-domain-links.spec.ts @@ -1,14 +1,19 @@ +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' +import { describe, expect, it } from 'vitest' import { - describe, it, expect -} from 'vitest' -import { RiviereQuery } from './RiviereQuery' -import { - createMinimalValidGraph, createAPIComponent, + createMinimalValidGraph, createUseCaseComponent, -} from '../../../platform/__fixtures__/riviere-graph-fixtures' +} from './__fixtures__/riviere-graph-fixtures' import { queryCrossDomainLinks } from './cross-domain-queries' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +import { RiviereQuery } from './RiviereQuery' + +function plainLinks(links: ReturnType) { + return links.map(({ targetDomain, linkType }) => ({ + targetDomain: targetDomain.value, + linkType, + })) +} describe('crossDomainLinks', () => { it('returns empty array when domain has no outgoing links to other domains', () => { @@ -17,7 +22,7 @@ describe('crossDomainLinks', () => { const result = query.crossDomainLinks('test') - expect(result).toStrictEqual([]) + expect(plainLinks(result)).toStrictEqual([]) }) it('returns unique outgoing links to other domains with link type', () => { @@ -63,7 +68,7 @@ describe('crossDomainLinks', () => { const result = query.crossDomainLinks('test') - expect(result).toStrictEqual([ + expect(plainLinks(result)).toStrictEqual([ { targetDomain: 'orders', linkType: 'sync', @@ -119,7 +124,7 @@ describe('crossDomainLinks', () => { const result = query.crossDomainLinks('test') - expect(result).toStrictEqual([ + expect(plainLinks(result)).toStrictEqual([ { targetDomain: 'orders', linkType: 'sync', @@ -171,7 +176,7 @@ describe('crossDomainLinks', () => { const result = query.crossDomainLinks('test') - expect(result).toStrictEqual([ + expect(plainLinks(result)).toStrictEqual([ { targetDomain: 'orders', linkType: 'async', @@ -206,7 +211,7 @@ describe('crossDomainLinks', () => { const result = query.crossDomainLinks('test') - expect(result).toStrictEqual([]) + expect(plainLinks(result)).toStrictEqual([]) }) it('returns results sorted by targetDomain', () => { @@ -252,7 +257,7 @@ describe('crossDomainLinks', () => { const result = query.crossDomainLinks('test') - expect(result.map((l) => l.targetDomain)).toStrictEqual(['alpha', 'zebra']) + expect(result.map((l) => l.targetDomain.value)).toStrictEqual(['alpha', 'zebra']) }) it('ignores links to non-existent components (defensive check)', () => { @@ -290,7 +295,7 @@ describe('crossDomainLinks', () => { const result = queryCrossDomainLinks(graph, 'test') - expect(result).toStrictEqual([]) + expect(plainLinks(result)).toStrictEqual([]) }) it('handles links with no explicit type (undefined linkType)', () => { @@ -319,7 +324,7 @@ describe('crossDomainLinks', () => { const result = query.crossDomainLinks('test') - expect(result).toStrictEqual([ + expect(plainLinks(result)).toStrictEqual([ { targetDomain: 'orders', linkType: undefined, @@ -388,7 +393,7 @@ describe('crossDomainLinks', () => { const result = query.crossDomainLinks('test') - expect(result).toStrictEqual([ + expect(plainLinks(result)).toStrictEqual([ { targetDomain: 'orders', linkType: undefined, diff --git a/packages/riviere-query/src/features/querying/queries/cross-domain-queries.ts b/packages/riviere-builder/domain-model/src/domain/query/cross-domain-queries.ts similarity index 82% rename from packages/riviere-query/src/features/querying/queries/cross-domain-queries.ts rename to packages/riviere-builder/domain-model/src/domain/query/cross-domain-queries.ts index 65a2524c1..9ebc10f0b 100644 --- a/packages/riviere-query/src/features/querying/queries/cross-domain-queries.ts +++ b/packages/riviere-builder/domain-model/src/domain/query/cross-domain-queries.ts @@ -1,15 +1,14 @@ -import type { RiviereGraph } from '@living-architecture/riviere-schema' -import type { - CrossDomainLink, DomainConnection -} from './domain-types' -import { parseDomainName } from './domain-types' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import { compareByCodePoint } from './compare-by-code-point' +import { CrossDomainLink } from './cross-domain-link' +import { DomainConnection } from './domain-connection' +import { DomainName } from './domain-name' function buildNodeIdToDomain(graph: RiviereGraph): Map { return new Map(graph.components.map((c) => [c.id, c.domain])) } -/** @riviere-role query-model */ +/** @riviere-role domain-service */ export function queryCrossDomainLinks(graph: RiviereGraph, domainName: string): CrossDomainLink[] { const nodeIdToDomain = buildNodeIdToDomain(graph) const seen = new Set() @@ -34,10 +33,12 @@ export function queryCrossDomainLinks(graph: RiviereGraph, domainName: string): } seen.add(key) - results.push({ - targetDomain: parseDomainName(targetDomain), - linkType: link.type, - }) + results.push( + CrossDomainLink.parse({ + targetDomain: DomainName.parse(targetDomain), + linkType: link.type, + }), + ) } return results.sort(compareCrossDomainLinks) @@ -51,7 +52,7 @@ function linkTypeForSort(linkType: 'sync' | 'async' | undefined): string { } function compareCrossDomainLinks(a: CrossDomainLink, b: CrossDomainLink): number { - const domainCompare = compareByCodePoint(a.targetDomain, b.targetDomain) + const domainCompare = compareByCodePoint(a.targetDomain.value, b.targetDomain.value) if (domainCompare !== 0) return domainCompare return compareByCodePoint(linkTypeForSort(a.linkType), linkTypeForSort(b.linkType)) } @@ -127,28 +128,28 @@ function toConnectionResults( connections: Map, direction: 'outgoing' | 'incoming', ): DomainConnection[] { - return Array.from(connections.entries()).map(([domain, counts]) => ({ - targetDomain: parseDomainName(domain), - direction, - apiCount: counts.apiCount, - eventCount: counts.eventCount, - })) + return Array.from(connections.entries()).map(([domain, counts]) => + DomainConnection.parse({ + targetDomain: DomainName.parse(domain), + direction, + apiCount: counts.apiCount, + eventCount: counts.eventCount, + }), + ) } -/** @riviere-role query-model */ +/** @riviere-role domain-service */ export function queryDomainConnections( graph: RiviereGraph, domainName: string, ): DomainConnection[] { const nodeIdToDomain = buildNodeIdToDomain(graph) const nodeById = new Map(graph.components.map((c) => [c.id, { type: c.type }])) - const { - outgoing, incoming - } = collectConnections(graph, domainName, nodeIdToDomain, nodeById) + const { outgoing, incoming } = collectConnections(graph, domainName, nodeIdToDomain, nodeById) const results = [ ...toConnectionResults(outgoing, 'outgoing'), ...toConnectionResults(incoming, 'incoming'), ] - return results.sort((a, b) => compareByCodePoint(a.targetDomain, b.targetDomain)) + return results.sort((a, b) => compareByCodePoint(a.targetDomain.value, b.targetDomain.value)) } diff --git a/packages/riviere-builder/domain-model/src/domain/query/depth-queries.ts b/packages/riviere-builder/domain-model/src/domain/query/depth-queries.ts new file mode 100644 index 000000000..bcd9e8088 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/depth-queries.ts @@ -0,0 +1,83 @@ +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' +import { ComponentDepths } from './component-depths' +import { isEntryPointType } from './flow-constants' + +interface DepthQueueEntry { + id: string + depth: number +} + +/** @riviere-role domain-service */ +export function queryNodeDepths(graph: RiviereGraph): ComponentDepths { + const depths = new Map() + + const entryPoints = findEntryPointIds(graph) + if (entryPoints.length === 0) { + return ComponentDepths.parse(depths) + } + + const outgoingEdges = buildOutgoingEdges(graph) + const queue: DepthQueueEntry[] = entryPoints.map((id) => ({ + id, + depth: 0, + })) + + processQueue(queue, depths, outgoingEdges) + + return ComponentDepths.parse(depths) +} + +function processQueue( + queue: DepthQueueEntry[], + depths: Map, + outgoingEdges: Map, +): void { + const current = queue.shift() + if (current === undefined) return + + const existingDepth = depths.get(current.id) + const shouldProcess = existingDepth === undefined || existingDepth > current.depth + + if (shouldProcess) { + depths.set(current.id, current.depth) + enqueueChildren(outgoingEdges, current, queue) + } + + processQueue(queue, depths, outgoingEdges) +} + +function enqueueChildren( + outgoingEdges: Map, + current: DepthQueueEntry, + queue: DepthQueueEntry[], +): void { + const edges = outgoingEdges.get(current.id) + if (edges) { + for (const targetId of edges) { + queue.push({ + id: targetId, + depth: current.depth + 1, + }) + } + } +} + +function findEntryPointIds(graph: RiviereGraph): string[] { + const targets = new Set(graph.links.map((link) => link.target)) + return graph.components + .filter((c) => isEntryPointType(c.type) && !targets.has(c.id)) + .map((c) => c.id) +} + +function buildOutgoingEdges(graph: RiviereGraph): Map { + const edges = new Map() + for (const link of graph.links) { + const existing = edges.get(link.source) + if (existing) { + existing.push(link.target) + } else { + edges.set(link.source, [link.target]) + } + } + return edges +} diff --git a/packages/riviere-builder/domain-model/src/domain/query/diff-stats.ts b/packages/riviere-builder/domain-model/src/domain/query/diff-stats.ts new file mode 100644 index 000000000..790287c97 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/diff-stats.ts @@ -0,0 +1,33 @@ +/** @riviere-role value-object */ +export class DiffStats { + declare private readonly brand: 'DiffStats' + readonly componentsAdded: number + readonly componentsRemoved: number + readonly componentsModified: number + readonly linksAdded: number + readonly linksRemoved: number + + private constructor(input: { + readonly componentsAdded: number + readonly componentsRemoved: number + readonly componentsModified: number + readonly linksAdded: number + readonly linksRemoved: number + }) { + this.componentsAdded = input.componentsAdded + this.componentsRemoved = input.componentsRemoved + this.componentsModified = input.componentsModified + this.linksAdded = input.linksAdded + this.linksRemoved = input.linksRemoved + } + + static parse(input: { + readonly componentsAdded: number + readonly componentsRemoved: number + readonly componentsModified: number + readonly linksAdded: number + readonly linksRemoved: number + }): DiffStats { + return new DiffStats(input) + } +} diff --git a/packages/riviere-query/src/features/querying/queries/diff.spec.ts b/packages/riviere-builder/domain-model/src/domain/query/diff.spec.ts similarity index 95% rename from packages/riviere-query/src/features/querying/queries/diff.spec.ts rename to packages/riviere-builder/domain-model/src/domain/query/diff.spec.ts index 1eb4b1730..220caa610 100644 --- a/packages/riviere-query/src/features/querying/queries/diff.spec.ts +++ b/packages/riviere-builder/domain-model/src/domain/query/diff.spec.ts @@ -1,14 +1,12 @@ +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' +import { describe, expect, it } from 'vitest' import { - describe, it, expect -} from 'vitest' -import { RiviereQuery } from './RiviereQuery' -import { + assertDefined, + createAPIComponent, createMinimalValidGraph, defaultSourceLocation, - createAPIComponent, - assertDefined, -} from '../../../platform/__fixtures__/riviere-graph-fixtures' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +} from './__fixtures__/riviere-graph-fixtures' +import { RiviereQuery } from './RiviereQuery' describe('diff', () => { describe('components', () => { @@ -71,7 +69,7 @@ describe('diff', () => { const result = query.diff(otherGraph) expect(result.components.modified).toHaveLength(1) - expect(result.components.modified[0]?.id).toBe('test:mod:ui:page') + expect(result.components.modified[0]?.id.value).toBe('test:mod:ui:page') }) it('includes name in changedFields when component name changes', () => { @@ -104,7 +102,7 @@ describe('diff', () => { const query = new RiviereQuery(graph) const result = query.diff(graph) - expect(result).toStrictEqual({ + expect(JSON.parse(JSON.stringify(result))).toStrictEqual({ components: { added: [], removed: [], diff --git a/packages/riviere-builder/domain-model/src/domain/query/domain-connection.ts b/packages/riviere-builder/domain-model/src/domain/query/domain-connection.ts new file mode 100644 index 000000000..4d14e360b --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/domain-connection.ts @@ -0,0 +1,31 @@ +import { DomainName } from './domain-name' + +/** @riviere-role value-object */ +export class DomainConnection { + declare private readonly brand: 'DomainConnection' + readonly targetDomain: DomainName + readonly direction: 'outgoing' | 'incoming' + readonly apiCount: number + readonly eventCount: number + + private constructor(input: { + readonly targetDomain: DomainName + readonly direction: 'outgoing' | 'incoming' + readonly apiCount: number + readonly eventCount: number + }) { + this.targetDomain = input.targetDomain + this.direction = input.direction + this.apiCount = input.apiCount + this.eventCount = input.eventCount + } + + static parse(input: { + readonly targetDomain: DomainName + readonly direction: 'outgoing' | 'incoming' + readonly apiCount: number + readonly eventCount: number + }): DomainConnection { + return new DomainConnection(input) + } +} diff --git a/packages/riviere-query/src/features/querying/queries/domain-connections.spec.ts b/packages/riviere-builder/domain-model/src/domain/query/domain-connections.spec.ts similarity index 91% rename from packages/riviere-query/src/features/querying/queries/domain-connections.spec.ts rename to packages/riviere-builder/domain-model/src/domain/query/domain-connections.spec.ts index 3321678bb..b7467f0c6 100644 --- a/packages/riviere-query/src/features/querying/queries/domain-connections.spec.ts +++ b/packages/riviere-builder/domain-model/src/domain/query/domain-connections.spec.ts @@ -1,13 +1,11 @@ +import { describe, expect, it } from 'vitest' import { - describe, it, expect -} from 'vitest' -import { RiviereQuery } from './RiviereQuery' -import { - createMinimalValidGraph, createAPIComponent, - createUseCaseComponent, createEventHandlerComponent, -} from '../../../platform/__fixtures__/riviere-graph-fixtures' + createMinimalValidGraph, + createUseCaseComponent, +} from './__fixtures__/riviere-graph-fixtures' +import { RiviereQuery } from './RiviereQuery' describe('domainConnections', () => { it('returns empty array when domain has no connections to other domains', () => { @@ -16,7 +14,7 @@ describe('domainConnections', () => { const result = query.domainConnections('test') - expect(result).toStrictEqual([]) + expect(JSON.parse(JSON.stringify(result))).toStrictEqual([]) }) it('returns outgoing connections with API counts when calling other domain APIs', () => { @@ -58,7 +56,7 @@ describe('domainConnections', () => { const result = query.domainConnections('test') - expect(result).toStrictEqual([ + expect(JSON.parse(JSON.stringify(result))).toStrictEqual([ { targetDomain: 'orders', direction: 'outgoing', @@ -95,7 +93,7 @@ describe('domainConnections', () => { const result = query.domainConnections('test') - expect(result).toStrictEqual([ + expect(JSON.parse(JSON.stringify(result))).toStrictEqual([ { targetDomain: 'notifications', direction: 'outgoing', @@ -132,7 +130,7 @@ describe('domainConnections', () => { const result = query.domainConnections('test') - expect(result).toStrictEqual([ + expect(JSON.parse(JSON.stringify(result))).toStrictEqual([ { targetDomain: 'orders', direction: 'incoming', @@ -186,13 +184,13 @@ describe('domainConnections', () => { const result = query.domainConnections('test') - expect(result).toContainEqual({ + expect(JSON.parse(JSON.stringify(result))).toContainEqual({ targetDomain: 'orders', direction: 'outgoing', apiCount: 1, eventCount: 0, }) - expect(result).toContainEqual({ + expect(JSON.parse(JSON.stringify(result))).toContainEqual({ targetDomain: 'orders', direction: 'incoming', apiCount: 1, @@ -243,7 +241,7 @@ describe('domainConnections', () => { const result = query.domainConnections('test') - expect(result.map((c) => c.targetDomain)).toStrictEqual(['alpha', 'zebra']) + expect(result.map((c) => c.targetDomain.value)).toStrictEqual(['alpha', 'zebra']) }) it('excludes links within the same domain from counts', () => { @@ -269,6 +267,6 @@ describe('domainConnections', () => { const result = query.domainConnections('test') - expect(result).toStrictEqual([]) + expect(JSON.parse(JSON.stringify(result))).toStrictEqual([]) }) }) diff --git a/packages/riviere-builder/domain-model/src/domain/query/domain-name.ts b/packages/riviere-builder/domain-model/src/domain/query/domain-name.ts new file mode 100644 index 000000000..96f7002bf --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/domain-name.ts @@ -0,0 +1,22 @@ +import { z } from 'zod' + +const schema = z.string() + +/** @riviere-role value-object */ +export class DomainName { + declare private readonly brand: 'DomainName' + + private constructor(readonly value: string) {} + + static parse(value: string): DomainName { + return new DomainName(schema.parse(value)) + } + + localeCompare(other: DomainName): number { + return this.value.localeCompare(other.value) + } + + toJSON(): string { + return this.value + } +} diff --git a/packages/riviere-query/src/features/querying/queries/domain-queries.ts b/packages/riviere-builder/domain-model/src/domain/query/domain-queries.ts similarity index 78% rename from packages/riviere-query/src/features/querying/queries/domain-queries.ts rename to packages/riviere-builder/domain-model/src/domain/query/domain-queries.ts index 4bc8117f2..1c0559043 100644 --- a/packages/riviere-query/src/features/querying/queries/domain-queries.ts +++ b/packages/riviere-builder/domain-model/src/domain/query/domain-queries.ts @@ -1,23 +1,24 @@ import type { - RiviereGraph, DomainOpComponent -} from '@living-architecture/riviere-schema' -import { Entity } from './event-types' -import type { EntityTransition } from './event-types' -import type { - State, Domain, ComponentCounts -} from './domain-types' -import { - parseEntityName, parseDomainName, parseState, parseOperationName -} from './domain-types' -import { componentsInDomain } from './component-queries' + DomainOpComponent, + RiviereGraph, +} from '@living-architecture/riviere-schema-published-language/schema' import { compareByCodePoint } from './compare-by-code-point' +import { ComponentCounts } from './component-counts' +import { componentsInDomain } from './component-queries' +import { Domain } from './domain' +import { DomainName } from './domain-name' +import { Entity } from './entity' +import { EntityName } from './entity-name' +import { EntityTransition } from './entity-transition' +import { OperationName } from './operation-name' +import { State } from './state' -/** @riviere-role query-model */ +/** @riviere-role domain-service */ export function queryDomains(graph: RiviereGraph): Domain[] { return Object.entries(graph.metadata.domains).map(([name, metadata]) => { const dc = componentsInDomain(graph, name) const count = (type: string): number => dc.filter((c) => c.type === type).length - const componentCounts: ComponentCounts = { + const componentCounts: ComponentCounts = ComponentCounts.parse({ UI: count('UI'), API: count('API'), UseCase: count('UseCase'), @@ -26,17 +27,17 @@ export function queryDomains(graph: RiviereGraph): Domain[] { EventHandler: count('EventHandler'), Custom: count('Custom'), total: dc.length, - } - return { + }) + return Domain.parse({ name, description: metadata.description, systemType: metadata.systemType, componentCounts, - } + }) }) } -/** @riviere-role query-model */ +/** @riviere-role domain-service */ export function operationsForEntity(graph: RiviereGraph, entityName: string): DomainOpComponent[] { return graph.components.filter( (c): c is DomainOpComponent => c.type === 'DomainOp' && c.entity === entityName, @@ -49,7 +50,7 @@ interface PartialEntity { operations: DomainOpComponent[] } -/** @riviere-role query-model */ +/** @riviere-role domain-service */ export function queryEntities(graph: RiviereGraph, domainName?: string): Entity[] { const domainOps = graph.components.filter( (c): c is DomainOpComponent & { entity: string } => @@ -82,9 +83,9 @@ function createEntity(graph: RiviereGraph, partial: PartialEntity): Entity { const sortedOperations = [...partial.operations].sort((a, b) => compareByCodePoint(a.operationName, b.operationName), ) - return new Entity( - parseEntityName(partial.name), - parseDomainName(partial.domain), + return Entity.parse( + EntityName.parse(partial.name), + DomainName.parse(partial.domain), sortedOperations, statesForEntity(graph, partial.name), transitionsForEntity(graph, partial.name), @@ -92,7 +93,7 @@ function createEntity(graph: RiviereGraph, partial: PartialEntity): Entity { ) } -/** @riviere-role query-model */ +/** @riviere-role domain-service */ export function businessRulesForEntity(graph: RiviereGraph, entityName: string): string[] { const operations = operationsForEntity(graph, entityName) const allRules: string[] = [] @@ -103,24 +104,26 @@ export function businessRulesForEntity(graph: RiviereGraph, entityName: string): return [...new Set(allRules)] } -/** @riviere-role query-model */ +/** @riviere-role domain-service */ export function transitionsForEntity(graph: RiviereGraph, entityName: string): EntityTransition[] { const operations = operationsForEntity(graph, entityName) const transitions: EntityTransition[] = [] for (const op of operations) { if (op.stateChanges === undefined) continue for (const sc of op.stateChanges) { - transitions.push({ - from: parseState(sc.from), - to: parseState(sc.to), - triggeredBy: parseOperationName(op.operationName), - }) + transitions.push( + EntityTransition.parse({ + from: State.parse(sc.from), + to: State.parse(sc.to), + triggeredBy: OperationName.parse(op.operationName), + }), + ) } } return transitions } -/** @riviere-role query-model */ +/** @riviere-role domain-service */ export function statesForEntity(graph: RiviereGraph, entityName: string): State[] { const operations = operationsForEntity(graph, entityName) const states = new Set() @@ -153,13 +156,13 @@ function orderStatesByTransitions(states: Set, operations: DomainOpCompo const follow = (s: string): void => { if (visited.has(s)) return visited.add(s) - ordered.push(parseState(s)) + ordered.push(State.parse(s)) const next = transitionMap.get(s) if (next) follow(next) } ;[...fromStates].filter((s) => !toStates.has(s)).forEach(follow) states.forEach((s) => { - if (!visited.has(s)) ordered.push(parseState(s)) + if (!visited.has(s)) ordered.push(State.parse(s)) }) return ordered } diff --git a/packages/riviere-builder/domain-model/src/domain/query/domain.ts b/packages/riviere-builder/domain-model/src/domain/query/domain.ts new file mode 100644 index 000000000..ec72f4f01 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/domain.ts @@ -0,0 +1,31 @@ +import { ComponentCounts } from './component-counts' + +/** @riviere-role value-object */ +export class Domain { + declare private readonly brand: 'Domain' + readonly name: string + readonly description: string + readonly systemType: import('@living-architecture/riviere-schema-published-language/schema').SystemType + readonly componentCounts: ComponentCounts + + private constructor(input: { + readonly name: string + readonly description: string + readonly systemType: import('@living-architecture/riviere-schema-published-language/schema').SystemType + readonly componentCounts: ComponentCounts + }) { + this.name = input.name + this.description = input.description + this.systemType = input.systemType + this.componentCounts = input.componentCounts + } + + static parse(input: { + readonly name: string + readonly description: string + readonly systemType: import('@living-architecture/riviere-schema-published-language/schema').SystemType + readonly componentCounts: ComponentCounts + }): Domain { + return new Domain(input) + } +} diff --git a/packages/riviere-builder/domain-model/src/domain/query/domains.spec.ts b/packages/riviere-builder/domain-model/src/domain/query/domains.spec.ts new file mode 100644 index 000000000..008e92e0b --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/domains.spec.ts @@ -0,0 +1,158 @@ +import { describe, expect, it } from 'vitest' +import { + assertDefined, + createAPIComponent, + createMinimalValidGraph, + createUseCaseComponent, +} from './__fixtures__/riviere-graph-fixtures' +import { RiviereQuery } from './RiviereQuery' + +describe('domains', () => { + it('returns domain with name, description, and systemType from metadata', () => { + const graph = createMinimalValidGraph() + const query = new RiviereQuery(graph) + + const result = query.domains() + + expect(JSON.parse(JSON.stringify(result))).toStrictEqual([ + { + name: 'test', + description: 'Test domain', + systemType: 'domain', + componentCounts: { + UI: 1, + API: 0, + UseCase: 0, + DomainOp: 0, + Event: 0, + EventHandler: 0, + Custom: 0, + total: 1, + }, + }, + ]) + }) + + it('returns the external-service system type from metadata', () => { + const graph = createMinimalValidGraph() + graph.metadata.domains['alerts'] = { + description: 'External alert service', + systemType: 'external-service', + } + const query = new RiviereQuery(graph) + + const result = query.domains() + + expect(result.find((domain) => domain.name === 'alerts')?.systemType).toBe('external-service') + }) + + it('returns multiple domains with correct component counts per type', () => { + const graph = createMinimalValidGraph() + graph.metadata.domains['orders'] = { + description: 'Order management', + systemType: 'domain', + } + graph.metadata.domains['shipping'] = { + description: 'Shipping integration', + systemType: 'bff', + } + graph.components.push( + createAPIComponent({ + id: 'orders:api:create', + name: 'Create Order', + domain: 'orders', + }), + createAPIComponent({ + id: 'orders:api:get', + name: 'Get Order', + domain: 'orders', + }), + createUseCaseComponent({ + id: 'orders:usecase:checkout', + name: 'Checkout', + domain: 'orders', + }), + createAPIComponent({ + id: 'shipping:api:track', + name: 'Track', + domain: 'shipping', + }), + ) + const query = new RiviereQuery(graph) + + const result = query.domains() + + const orders = result.find((d) => d.name === 'orders') + expect(JSON.parse(JSON.stringify(orders))).toStrictEqual({ + name: 'orders', + description: 'Order management', + systemType: 'domain', + componentCounts: { + UI: 0, + API: 2, + UseCase: 1, + DomainOp: 0, + Event: 0, + EventHandler: 0, + Custom: 0, + total: 3, + }, + }) + + const shipping = result.find((d) => d.name === 'shipping') + expect(shipping?.systemType).toBe('bff') + expect(shipping?.componentCounts.API).toBe(1) + expect(shipping?.componentCounts.total).toBe(1) + }) + + it('throws when graph has no domains (invalid per schema)', () => { + const graph = createMinimalValidGraph() + graph.metadata.domains = {} + graph.components = [] + + expect(() => new RiviereQuery(graph)).toThrow(/must NOT have fewer than 1 properties/i) + }) + + it('does not include external systems in domains (use externalSystems() instead)', () => { + const graph = createMinimalValidGraph() + graph.externalLinks = [ + { + source: 'test:mod:ui:page', + target: { name: 'Stripe' }, + type: 'sync', + }, + { + source: 'test:mod:ui:page', + target: { name: 'Twilio' }, + type: 'async', + }, + ] + const query = new RiviereQuery(graph) + + const result = query.domains() + + expect(result.find((d) => d.name === 'external')).toBeUndefined() + expect(result.find((d) => d.name === 'Stripe')).toBeUndefined() + expect(result.find((d) => d.name === 'Twilio')).toBeUndefined() + }) +}) + +describe('assertDefined', () => { + it('returns value when defined', () => { + const value = assertDefined('test') + + expect(value).toBe('test') + }) + + it('throws when value is undefined', () => { + expect(() => assertDefined(undefined)).toThrow('Expected value to be defined') + }) + + it('throws when value is null', () => { + expect(() => assertDefined(null)).toThrow('Expected value to be defined') + }) + + it('throws with custom message', () => { + expect(() => assertDefined(null, 'Custom error')).toThrow('Custom error') + }) +}) diff --git a/packages/riviere-query/src/features/querying/queries/entities.spec.ts b/packages/riviere-builder/domain-model/src/domain/query/entities.spec.ts similarity index 96% rename from packages/riviere-query/src/features/querying/queries/entities.spec.ts rename to packages/riviere-builder/domain-model/src/domain/query/entities.spec.ts index 63027ace0..4b865feb9 100644 --- a/packages/riviere-query/src/features/querying/queries/entities.spec.ts +++ b/packages/riviere-builder/domain-model/src/domain/query/entities.spec.ts @@ -1,8 +1,8 @@ -import { RiviereQuery } from './RiviereQuery' import { - createMinimalValidGraph, createDomainOpComponent, -} from '../../../platform/__fixtures__/riviere-graph-fixtures' + createMinimalValidGraph, +} from './__fixtures__/riviere-graph-fixtures' +import { RiviereQuery } from './RiviereQuery' describe('operationsFor', () => { it('returns empty array when entity does not exist', () => { @@ -70,7 +70,7 @@ describe('entities', () => { const entities = query.entities() - expect(entities).toMatchObject([ + expect(JSON.parse(JSON.stringify(entities))).toMatchObject([ { name: 'Order', domain: 'orders', @@ -103,7 +103,7 @@ describe('entities', () => { const entities = query.entities('orders') - expect(entities).toMatchObject([ + expect(JSON.parse(JSON.stringify(entities))).toMatchObject([ { name: 'Order', domain: 'orders', @@ -136,7 +136,7 @@ describe('entities', () => { const entities = query.entities() - expect(entities.map((e) => e.name)).toStrictEqual(['Apple', 'Zebra']) + expect(entities.map((e) => e.name.value)).toStrictEqual(['Apple', 'Zebra']) }) it('returns entity with states ordered by transition flow', () => { @@ -173,7 +173,11 @@ describe('entities', () => { const entities = query.entities() expect(entities).toHaveLength(1) - expect(entities[0]?.states).toStrictEqual(['Draft', 'Placed', 'Confirmed']) + expect(entities[0]?.states.map((state) => state.value)).toStrictEqual([ + 'Draft', + 'Placed', + 'Confirmed', + ]) }) it('returns entity with transitions including triggeredBy operation', () => { @@ -197,7 +201,7 @@ describe('entities', () => { const entities = query.entities() expect(entities).toHaveLength(1) - expect(entities[0]?.transitions).toStrictEqual([ + expect(JSON.parse(JSON.stringify(entities[0]?.transitions))).toStrictEqual([ { from: 'Draft', to: 'Placed', diff --git a/packages/riviere-builder/domain-model/src/domain/query/entity-name.ts b/packages/riviere-builder/domain-model/src/domain/query/entity-name.ts new file mode 100644 index 000000000..76f715046 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/entity-name.ts @@ -0,0 +1,18 @@ +import { z } from 'zod' + +const schema = z.string() + +/** @riviere-role value-object */ +export class EntityName { + declare private readonly brand: 'EntityName' + + private constructor(readonly value: string) {} + + static parse(value: string): EntityName { + return new EntityName(schema.parse(value)) + } + + toJSON(): string { + return this.value + } +} diff --git a/packages/riviere-builder/domain-model/src/domain/query/entity-transition.ts b/packages/riviere-builder/domain-model/src/domain/query/entity-transition.ts new file mode 100644 index 000000000..630b39e6b --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/entity-transition.ts @@ -0,0 +1,31 @@ +import type { OperationName } from './operation-name' +import type { State } from './state' + +/** + * A state transition in an entity's state machine. + * @riviere-role value-object + */ +export class EntityTransition { + declare private readonly brand: 'EntityTransition' + readonly from: State + readonly to: State + readonly triggeredBy: OperationName + + private constructor(input: { + readonly from: State + readonly to: State + readonly triggeredBy: OperationName + }) { + this.from = input.from + this.to = input.to + this.triggeredBy = input.triggeredBy + } + + static parse(input: { + readonly from: State + readonly to: State + readonly triggeredBy: OperationName + }): EntityTransition { + return new EntityTransition(input) + } +} diff --git a/packages/riviere-builder/domain-model/src/domain/query/entity.ts b/packages/riviere-builder/domain-model/src/domain/query/entity.ts new file mode 100644 index 000000000..187b3e77b --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/entity.ts @@ -0,0 +1,42 @@ +import type { DomainOpComponent } from '@living-architecture/riviere-schema-published-language/schema' +import type { DomainName } from './domain-name' +import type { EntityName } from './entity-name' +import { EntityTransition } from './entity-transition' +import type { State } from './state' + +/** @riviere-role value-object */ +export class Entity { + declare private readonly brand: 'Entity' + + static parse( + name: EntityName, + domain: DomainName, + operations: readonly DomainOpComponent[], + states: readonly State[], + transitions: readonly EntityTransition[], + businessRules: readonly string[], + ): Entity { + return new Entity(name, domain, operations, states, transitions, businessRules) + } + + private constructor( + public readonly name: EntityName, + public readonly domain: DomainName, + public readonly operations: readonly DomainOpComponent[], + public readonly states: readonly State[], + public readonly transitions: readonly EntityTransition[], + public readonly businessRules: readonly string[], + ) {} + + hasStates(): boolean { + return this.states.length > 0 + } + + hasBusinessRules(): boolean { + return this.businessRules.length > 0 + } + + firstOperationId(): string | undefined { + return this.operations[0]?.id + } +} diff --git a/packages/riviere-builder/domain-model/src/domain/query/errors.spec.ts b/packages/riviere-builder/domain-model/src/domain/query/errors.spec.ts new file mode 100644 index 000000000..a1929e38c --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/errors.spec.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from 'vitest' +import { ComponentNotFoundError } from './errors' + +describe('ComponentNotFoundError', () => { + it('has message containing component ID', () => { + const error = new ComponentNotFoundError('orders:checkout:api:place-order') + + expect(error.message).toBe("Component 'orders:checkout:api:place-order' not found") + }) + + it('exposes componentId property', () => { + const error = new ComponentNotFoundError('orders:checkout:api:place-order') + + expect(error.componentId).toBe('orders:checkout:api:place-order') + }) + + it('defaults suggestions to empty array', () => { + const error = new ComponentNotFoundError('orders:checkout:api:place-order') + + expect(error.suggestions).toStrictEqual([]) + }) + + it('exposes provided suggestions', () => { + const suggestions = ['orders:checkout:api:create-order', 'orders:checkout:api:get-order'] + const error = new ComponentNotFoundError('orders:checkout:api:place-ordr', suggestions) + + expect(error.suggestions).toStrictEqual(suggestions) + }) + + it('has name ComponentNotFoundError', () => { + const error = new ComponentNotFoundError('any:id') + + expect(error.name).toBe('ComponentNotFoundError') + }) +}) diff --git a/packages/riviere-builder/domain-model/src/domain/query/errors.ts b/packages/riviere-builder/domain-model/src/domain/query/errors.ts new file mode 100644 index 000000000..c6fb2aae1 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/errors.ts @@ -0,0 +1,23 @@ +/** @riviere-role domain-error */ +export class ComponentNotFoundError extends Error { + readonly componentId: string + readonly suggestions: string[] + + constructor(componentId: string, suggestions: string[] = []) { + super(`Component '${componentId}' not found`) + this.name = 'ComponentNotFoundError' + this.componentId = componentId + this.suggestions = suggestions + } +} + +/** @riviere-role domain-error */ +export class InvalidRiviereGraphError extends Error { + readonly issues: readonly string[] + + constructor(issues: readonly string[]) { + super(`Invalid RiviereGraph:\n${issues.join('\n')}`) + this.name = 'InvalidRiviereGraphError' + this.issues = issues + } +} diff --git a/packages/riviere-builder/domain-model/src/domain/query/event-handler-info.ts b/packages/riviere-builder/domain-model/src/domain/query/event-handler-info.ts new file mode 100644 index 000000000..dce8ed480 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/event-handler-info.ts @@ -0,0 +1,45 @@ +import type { DomainName } from './domain-name' +import type { EventName } from './event-name' +import type { HandlerId } from './handler-id' +import type { HandlerName } from './handler-name' +import { KnownSourceEvent } from './known-source-event' +import { UnknownSourceEvent } from './unknown-source-event' + +type SubscribedEventWithDomain = KnownSourceEvent | UnknownSourceEvent + +/** + * Information about an event handler component. + * @riviere-role value-object + */ +export class EventHandlerInfo { + declare private readonly brand: 'EventHandlerInfo' + readonly id: HandlerId + readonly handlerName: HandlerName + readonly domain: DomainName + readonly subscribedEvents: EventName[] + readonly subscribedEventsWithDomain: SubscribedEventWithDomain[] + + private constructor(input: { + readonly id: HandlerId + readonly handlerName: HandlerName + readonly domain: DomainName + readonly subscribedEvents: EventName[] + readonly subscribedEventsWithDomain: SubscribedEventWithDomain[] + }) { + this.id = input.id + this.handlerName = input.handlerName + this.domain = input.domain + this.subscribedEvents = input.subscribedEvents + this.subscribedEventsWithDomain = input.subscribedEventsWithDomain + } + + static parse(input: { + readonly id: HandlerId + readonly handlerName: HandlerName + readonly domain: DomainName + readonly subscribedEvents: EventName[] + readonly subscribedEventsWithDomain: SubscribedEventWithDomain[] + }): EventHandlerInfo { + return new EventHandlerInfo(input) + } +} diff --git a/packages/riviere-builder/domain-model/src/domain/query/event-id.ts b/packages/riviere-builder/domain-model/src/domain/query/event-id.ts new file mode 100644 index 000000000..770442104 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/event-id.ts @@ -0,0 +1,18 @@ +import { z } from 'zod' + +const schema = z.string() + +/** @riviere-role value-object */ +export class EventId { + declare private readonly brand: 'EventId' + + private constructor(readonly value: string) {} + + static parse(value: string): EventId { + return new EventId(schema.parse(value)) + } + + toJSON(): string { + return this.value + } +} diff --git a/packages/riviere-builder/domain-model/src/domain/query/event-name.ts b/packages/riviere-builder/domain-model/src/domain/query/event-name.ts new file mode 100644 index 000000000..0c864ab58 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/event-name.ts @@ -0,0 +1,18 @@ +import { z } from 'zod' + +const schema = z.string() + +/** @riviere-role value-object */ +export class EventName { + declare private readonly brand: 'EventName' + + private constructor(readonly value: string) {} + + static parse(value: string): EventName { + return new EventName(schema.parse(value)) + } + + toJSON(): string { + return this.value + } +} diff --git a/packages/riviere-builder/domain-model/src/domain/query/event-queries.ts b/packages/riviere-builder/domain-model/src/domain/query/event-queries.ts new file mode 100644 index 000000000..c751bd024 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/event-queries.ts @@ -0,0 +1,94 @@ +import type { + EventComponent, + EventHandlerComponent, + RiviereGraph, +} from '@living-architecture/riviere-schema-published-language/schema' +import { DomainName } from './domain-name' +import { EventHandlerInfo } from './event-handler-info' +import { EventId } from './event-id' +import { EventName } from './event-name' +import { EventSubscriber } from './event-subscriber' +import { HandlerId } from './handler-id' +import { HandlerName } from './handler-name' +import { KnownSourceEvent } from './known-source-event' +import { PublishedEvent } from './published-event' +import { UnknownSourceEvent } from './unknown-source-event' + +/** @riviere-role domain-service */ +export function queryPublishedEvents(graph: RiviereGraph, domainName?: string): PublishedEvent[] { + const eventComponents = graph.components.filter((c): c is EventComponent => c.type === 'Event') + const filtered = domainName + ? eventComponents.filter((e) => e.domain === domainName) + : eventComponents + const handlers = graph.components.filter( + (c): c is EventHandlerComponent => c.type === 'EventHandler', + ) + + return filtered.map((event) => { + const subscribers: EventSubscriber[] = handlers + .filter((h) => h.subscribedEvents.includes(event.eventName)) + .map((h) => + EventSubscriber.parse({ + handlerId: HandlerId.parse(h.id), + handlerName: HandlerName.parse(h.name), + domain: DomainName.parse(h.domain), + }), + ) + return PublishedEvent.parse({ + id: EventId.parse(event.id), + eventName: EventName.parse(event.eventName), + domain: DomainName.parse(event.domain), + handlers: subscribers, + }) + }) +} + +/** @riviere-role domain-service */ +export function queryEventHandlers(graph: RiviereGraph, eventName?: string): EventHandlerInfo[] { + const eventByName = buildEventNameMap(graph) + const handlers = findEventHandlerComponents(graph) + const filtered = eventName + ? handlers.filter((h) => h.subscribedEvents.includes(eventName)) + : handlers + return filtered.map((h) => buildEventHandlerInfo(h, eventByName)) +} + +function buildEventNameMap(graph: RiviereGraph): Map { + return new Map( + graph.components + .filter((c): c is EventComponent => c.type === 'Event') + .map((e) => [e.eventName, e]), + ) +} + +function findEventHandlerComponents(graph: RiviereGraph): EventHandlerComponent[] { + return graph.components.filter((c): c is EventHandlerComponent => c.type === 'EventHandler') +} + +function buildEventHandlerInfo( + handler: EventHandlerComponent, + eventByName: Map, +): EventHandlerInfo { + const subscribedEventsWithDomain = handler.subscribedEvents.map( + (name): KnownSourceEvent | UnknownSourceEvent => { + const event = eventByName.get(name) + if (event) + return KnownSourceEvent.parse({ + eventName: EventName.parse(name), + sourceDomain: DomainName.parse(event.domain), + sourceKnown: true, + }) + return UnknownSourceEvent.parse({ + eventName: EventName.parse(name), + sourceKnown: false, + }) + }, + ) + return EventHandlerInfo.parse({ + id: HandlerId.parse(handler.id), + handlerName: HandlerName.parse(handler.name), + domain: DomainName.parse(handler.domain), + subscribedEvents: handler.subscribedEvents.map(EventName.parse), + subscribedEventsWithDomain, + }) +} diff --git a/packages/riviere-builder/domain-model/src/domain/query/event-subscriber.ts b/packages/riviere-builder/domain-model/src/domain/query/event-subscriber.ts new file mode 100644 index 000000000..a7b56ca1b --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/event-subscriber.ts @@ -0,0 +1,32 @@ +import type { DomainName } from './domain-name' +import type { HandlerId } from './handler-id' +import type { HandlerName } from './handler-name' + +/** + * An event handler that subscribes to an event. + * @riviere-role value-object + */ +export class EventSubscriber { + declare private readonly brand: 'EventSubscriber' + readonly handlerId: HandlerId + readonly handlerName: HandlerName + readonly domain: DomainName + + private constructor(input: { + readonly handlerId: HandlerId + readonly handlerName: HandlerName + readonly domain: DomainName + }) { + this.handlerId = input.handlerId + this.handlerName = input.handlerName + this.domain = input.domain + } + + static parse(input: { + readonly handlerId: HandlerId + readonly handlerName: HandlerName + readonly domain: DomainName + }): EventSubscriber { + return new EventSubscriber(input) + } +} diff --git a/packages/riviere-query/src/features/querying/queries/events.spec.ts b/packages/riviere-builder/domain-model/src/domain/query/events.spec.ts similarity index 92% rename from packages/riviere-query/src/features/querying/queries/events.spec.ts rename to packages/riviere-builder/domain-model/src/domain/query/events.spec.ts index 8eabaf3e6..2d208b5ae 100644 --- a/packages/riviere-query/src/features/querying/queries/events.spec.ts +++ b/packages/riviere-builder/domain-model/src/domain/query/events.spec.ts @@ -1,9 +1,9 @@ -import { RiviereQuery } from './RiviereQuery' import { - createMinimalValidGraph, createEventComponent, createEventHandlerComponent, -} from '../../../platform/__fixtures__/riviere-graph-fixtures' + createMinimalValidGraph, +} from './__fixtures__/riviere-graph-fixtures' +import { RiviereQuery } from './RiviereQuery' describe('publishedEvents', () => { it('returns empty array when no Event components exist', () => { @@ -12,7 +12,7 @@ describe('publishedEvents', () => { const events = query.publishedEvents() - expect(events).toStrictEqual([]) + expect(JSON.parse(JSON.stringify(events))).toStrictEqual([]) }) it('returns event with handlers that subscribe to it', () => { @@ -35,7 +35,7 @@ describe('publishedEvents', () => { const events = query.publishedEvents() - expect(events).toStrictEqual([ + expect(JSON.parse(JSON.stringify(events))).toStrictEqual([ { id: 'orders:events:OrderCreated', eventName: 'OrderCreated', @@ -79,7 +79,7 @@ describe('publishedEvents', () => { const events = query.publishedEvents('orders') - expect(events).toStrictEqual([ + expect(JSON.parse(JSON.stringify(events))).toStrictEqual([ { id: 'orders:events:OrderCreated', eventName: 'OrderCreated', @@ -97,7 +97,7 @@ describe('eventHandlers', () => { const handlers = query.eventHandlers() - expect(handlers).toStrictEqual([]) + expect(JSON.parse(JSON.stringify(handlers))).toStrictEqual([]) }) it('returns handler with subscribedEventsWithDomain including source domain', () => { @@ -128,7 +128,7 @@ describe('eventHandlers', () => { const handlers = query.eventHandlers() - expect(handlers).toStrictEqual([ + expect(JSON.parse(JSON.stringify(handlers))).toStrictEqual([ { id: 'shipping:handlers:OnOrderCreated', handlerName: 'On Order Created', @@ -163,7 +163,7 @@ describe('eventHandlers', () => { const handlers = query.eventHandlers() - expect(handlers).toStrictEqual([ + expect(JSON.parse(JSON.stringify(handlers))).toStrictEqual([ { id: 'shipping:handlers:OnUnknownEvent', handlerName: 'On Unknown Event', @@ -217,7 +217,7 @@ describe('eventHandlers', () => { const handlers = query.eventHandlers('OrderCreated') - expect(handlers).toStrictEqual([ + expect(JSON.parse(JSON.stringify(handlers))).toStrictEqual([ { id: 'shipping:handlers:OnOrderCreated', handlerName: 'On Order Created', diff --git a/packages/riviere-builder/domain-model/src/domain/query/external-domain.ts b/packages/riviere-builder/domain-model/src/domain/query/external-domain.ts new file mode 100644 index 000000000..aec9b3778 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/external-domain.ts @@ -0,0 +1,27 @@ +import { DomainName } from './domain-name' + +/** @riviere-role value-object */ +export class ExternalDomain { + declare private readonly brand: 'ExternalDomain' + readonly name: string + readonly sourceDomains: DomainName[] + readonly connectionCount: number + + private constructor(input: { + readonly name: string + readonly sourceDomains: DomainName[] + readonly connectionCount: number + }) { + this.name = input.name + this.sourceDomains = input.sourceDomains + this.connectionCount = input.connectionCount + } + + static parse(input: { + readonly name: string + readonly sourceDomains: DomainName[] + readonly connectionCount: number + }): ExternalDomain { + return new ExternalDomain(input) + } +} diff --git a/packages/riviere-query/src/features/querying/queries/external-system-queries.ts b/packages/riviere-builder/domain-model/src/domain/query/external-system-queries.ts similarity index 84% rename from packages/riviere-query/src/features/querying/queries/external-system-queries.ts rename to packages/riviere-builder/domain-model/src/domain/query/external-system-queries.ts index dbedcc89d..db48b8055 100644 --- a/packages/riviere-query/src/features/querying/queries/external-system-queries.ts +++ b/packages/riviere-builder/domain-model/src/domain/query/external-system-queries.ts @@ -1,7 +1,7 @@ -import type { RiviereGraph } from '@living-architecture/riviere-schema' -import type { ExternalDomain } from './domain-types' -import { parseDomainName } from './domain-types' +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' import { compareByCodePoint } from './compare-by-code-point' +import { DomainName } from './domain-name' +import { ExternalDomain } from './external-domain' interface ExternalDomainAccumulator { sourceDomains: Set @@ -45,11 +45,13 @@ function convertToExternalDomains( domains: Map, ): ExternalDomain[] { return Array.from(domains.entries()) - .map(([name, acc]) => ({ - name, - sourceDomains: Array.from(acc.sourceDomains).map((d) => parseDomainName(d)), - connectionCount: acc.connectionCount, - })) + .map(([name, acc]) => + ExternalDomain.parse({ + name, + sourceDomains: Array.from(acc.sourceDomains).map((d) => DomainName.parse(d)), + connectionCount: acc.connectionCount, + }), + ) .sort((a, b) => compareByCodePoint(a.name, b.name)) } @@ -61,7 +63,7 @@ function convertToExternalDomains( * * @param graph - The RiviereGraph to query * @returns Array of ExternalDomain objects, sorted alphabetically by name - * @riviere-role query-model + * @riviere-role domain-service */ export function queryExternalDomains(graph: RiviereGraph): ExternalDomain[] { if (graph.externalLinks === undefined || graph.externalLinks.length === 0) { diff --git a/packages/riviere-query/src/features/querying/queries/external-systems.spec.ts b/packages/riviere-builder/domain-model/src/domain/query/external-systems.spec.ts similarity index 92% rename from packages/riviere-query/src/features/querying/queries/external-systems.spec.ts rename to packages/riviere-builder/domain-model/src/domain/query/external-systems.spec.ts index 18c6823d6..1c9bf0ef4 100644 --- a/packages/riviere-query/src/features/querying/queries/external-systems.spec.ts +++ b/packages/riviere-builder/domain-model/src/domain/query/external-systems.spec.ts @@ -1,13 +1,11 @@ +import { describe, expect, it } from 'vitest' import { - describe, it, expect -} from 'vitest' -import { queryExternalDomains } from './external-system-queries' -import { - createMinimalValidGraph, createAPIComponent, + createMinimalValidGraph, createUseCaseComponent, -} from '../../../platform/__fixtures__/riviere-graph-fixtures' -import { parseDomainName } from './domain-types' +} from './__fixtures__/riviere-graph-fixtures' +import { DomainName } from './domain-name' +import { queryExternalDomains } from './external-system-queries' describe('queryExternalDomains', () => { it('returns empty array when graph has no external links', () => { @@ -68,7 +66,7 @@ describe('queryExternalDomains', () => { const result = queryExternalDomains(graph) - expect(result[0]?.sourceDomains).toStrictEqual([parseDomainName('orders')]) + expect(result[0]?.sourceDomains).toStrictEqual([DomainName.parse('orders')]) }) it('aggregates multiple source domains for same external domain', () => { @@ -107,7 +105,7 @@ describe('queryExternalDomains', () => { expect(result).toHaveLength(1) expect(result[0]?.name).toBe('Stripe') expect(result[0]?.sourceDomains.sort((a, b) => a.localeCompare(b))).toStrictEqual( - [parseDomainName('orders'), parseDomainName('payments')].sort((a, b) => a.localeCompare(b)), + [DomainName.parse('orders'), DomainName.parse('payments')].sort((a, b) => a.localeCompare(b)), ) }) @@ -181,7 +179,7 @@ describe('queryExternalDomains', () => { const result = queryExternalDomains(graph) - expect(result[0]?.sourceDomains).toStrictEqual([parseDomainName('orders')]) + expect(result[0]?.sourceDomains).toStrictEqual([DomainName.parse('orders')]) }) it('skips external links with unknown source component', () => { diff --git a/packages/riviere-builder/domain-model/src/domain/query/flow-constants.ts b/packages/riviere-builder/domain-model/src/domain/query/flow-constants.ts new file mode 100644 index 000000000..986a3dce1 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/flow-constants.ts @@ -0,0 +1,13 @@ +import type { ComponentType } from '@living-architecture/riviere-schema-published-language/schema' + +const entryPointTypes: ReadonlySet = new Set([ + 'UI', + 'API', + 'EventHandler', + 'Custom', +]) + +/** @riviere-role domain-service */ +export function isEntryPointType(componentType: ComponentType): boolean { + return entryPointTypes.has(componentType) +} diff --git a/packages/riviere-builder/domain-model/src/domain/query/flow-queries.ts b/packages/riviere-builder/domain-model/src/domain/query/flow-queries.ts new file mode 100644 index 000000000..ad562d888 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/flow-queries.ts @@ -0,0 +1,184 @@ +import type { + Component, + ExternalLink, + Link, + RiviereGraph, +} from '@living-architecture/riviere-schema-published-language/schema' +import { ComponentId } from './component-id' +import { componentById, searchComponents } from './component-queries' +import { ComponentNotFoundError } from './errors' +import { Flow } from './flow' +import { isEntryPointType } from './flow-constants' +import { FlowStep } from './flow-step' +import { LinkId } from './link-id' +import { createLinkKey } from './link-key' +import { SearchWithFlowOptions } from './search-with-flow-options' +import { SearchWithFlowResult } from './search-with-flow-result' + +/** @riviere-role domain-service */ +export function findEntryPoints(graph: RiviereGraph): Component[] { + const targets = new Set(graph.links.map((link) => link.target)) + return graph.components.filter((c) => isEntryPointType(c.type) && !targets.has(c.id)) +} + +/** @riviere-role domain-service */ +export function traceFlowFrom( + graph: RiviereGraph, + startComponentId: ComponentId, +): { + componentIds: ComponentId[] + linkIds: LinkId[] +} { + const component = componentById(graph, startComponentId.value) + if (!component) { + throw new ComponentNotFoundError(startComponentId.value) + } + + const visited = new Set() + const visitedLinks = new Set() + const queue: string[] = [startComponentId.value] + + while (queue.length > 0) { + const currentId = queue.shift() + if (currentId === undefined || visited.has(currentId)) continue + visited.add(currentId) + + for (const link of graph.links) { + if (link.source === currentId && !visited.has(link.target)) { + queue.push(link.target) + visitedLinks.add(createLinkKey(link).value) + } + if (link.target === currentId && !visited.has(link.source)) { + queue.push(link.source) + visitedLinks.add(createLinkKey(link).value) + } + } + } + + return { + componentIds: Array.from(visited, ComponentId.parse), + linkIds: Array.from(visitedLinks, LinkId.parse), + } +} + +/** @riviere-role domain-service */ +export function queryFlows(graph: RiviereGraph): Flow[] { + const componentByIdMap = new Map(graph.components.map((c) => [c.id, c])) + const outgoingEdges = buildOutgoingEdges(graph) + const externalLinksBySource = buildExternalLinksBySource(graph) + + const traceForward = (entryPointId: string): Flow['steps'] => { + const steps: Flow['steps'] = [] + const visited = new Set() + + const traverse = (nodeId: string, depth: number): void => { + if (visited.has(nodeId)) return + visited.add(nodeId) + + const component = componentByIdMap.get(nodeId) + if (!component) return + + const edges = outgoingEdges.get(nodeId) ?? [] + const externalLinks = externalLinksBySource.get(nodeId) ?? [] + + steps.push( + FlowStep.parse({ + component, + outgoingLinks: edges, + depth, + externalLinks, + }), + ) + + for (const edge of edges) { + traverse(edge.target, depth + 1) + } + } + + traverse(entryPointId, 0) + return steps + } + + return findEntryPoints(graph).map((entryPoint) => + Flow.parse({ + entryPoint, + steps: traceForward(entryPoint.id), + }), + ) +} + +function buildExternalLinksBySource(graph: RiviereGraph): Map { + const externalLinks = graph.externalLinks ?? [] + const bySource = new Map() + + for (const link of externalLinks) { + const existing = bySource.get(link.source) + if (existing) { + existing.push(link) + } else { + bySource.set(link.source, [link]) + } + } + + return bySource +} + +function buildOutgoingEdges(graph: RiviereGraph): Map { + const edges = new Map() + for (const link of graph.links) { + const existing = edges.get(link.source) + if (existing) { + existing.push(link) + } else { + edges.set(link.source, [link]) + } + } + return edges +} + +/** @riviere-role domain-service */ +export function searchWithFlowContext( + graph: RiviereGraph, + query: string, + options: SearchWithFlowOptions, +): SearchWithFlowResult { + const trimmedQuery = query.trim().toLowerCase() + const isEmptyQuery = trimmedQuery === '' + + if (isEmptyQuery) { + if (options.returnAllOnEmptyQuery) { + const allIds = graph.components.map((c) => ComponentId.parse(c.id)) + return SearchWithFlowResult.parse({ + matchingIds: allIds, + visibleIds: allIds, + }) + } + return SearchWithFlowResult.parse({ + matchingIds: [], + visibleIds: [], + }) + } + + const matchingComponents = searchComponents(graph, query) + if (matchingComponents.length === 0) { + return SearchWithFlowResult.parse({ + matchingIds: [], + visibleIds: [], + }) + } + + const matchingIds = matchingComponents.map((c) => ComponentId.parse(c.id)) + const visibleIds = new Set() + + for (const component of matchingComponents) { + const flow = traceFlowFrom(graph, ComponentId.parse(component.id)) + for (const id of flow.componentIds) { + visibleIds.add(id) + } + } + + return SearchWithFlowResult.parse({ + matchingIds, + visibleIds: Array.from(visibleIds), + }) +} diff --git a/packages/riviere-builder/domain-model/src/domain/query/flow-step.ts b/packages/riviere-builder/domain-model/src/domain/query/flow-step.ts new file mode 100644 index 000000000..f9e16d4f0 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/flow-step.ts @@ -0,0 +1,35 @@ +import type { + Component, + ExternalLink, + Link, +} from '@living-architecture/riviere-schema-published-language/schema' + +/** @riviere-role value-object */ +export class FlowStep { + declare private readonly brand: 'FlowStep' + readonly component: Component + readonly outgoingLinks: Link[] + readonly depth: number + readonly externalLinks: ExternalLink[] + + private constructor(input: { + readonly component: Component + readonly outgoingLinks: Link[] + readonly depth: number + readonly externalLinks: ExternalLink[] + }) { + this.component = input.component + this.outgoingLinks = input.outgoingLinks + this.depth = input.depth + this.externalLinks = input.externalLinks + } + + static parse(input: { + readonly component: Component + readonly outgoingLinks: Link[] + readonly depth: number + readonly externalLinks: ExternalLink[] + }): FlowStep { + return new FlowStep(input) + } +} diff --git a/packages/riviere-builder/domain-model/src/domain/query/flow.ts b/packages/riviere-builder/domain-model/src/domain/query/flow.ts new file mode 100644 index 000000000..10b78d4cb --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/flow.ts @@ -0,0 +1,18 @@ +import type { Component } from '@living-architecture/riviere-schema-published-language/schema' +import { FlowStep } from './flow-step' + +/** @riviere-role value-object */ +export class Flow { + declare private readonly brand: 'Flow' + readonly entryPoint: Component + readonly steps: FlowStep[] + + private constructor(input: { readonly entryPoint: Component; readonly steps: FlowStep[] }) { + this.entryPoint = input.entryPoint + this.steps = input.steps + } + + static parse(input: { readonly entryPoint: Component; readonly steps: FlowStep[] }): Flow { + return new Flow(input) + } +} diff --git a/packages/riviere-query/src/features/querying/queries/flows.spec.ts b/packages/riviere-builder/domain-model/src/domain/query/flows.spec.ts similarity index 87% rename from packages/riviere-query/src/features/querying/queries/flows.spec.ts rename to packages/riviere-builder/domain-model/src/domain/query/flows.spec.ts index f620f4fb8..23969af32 100644 --- a/packages/riviere-query/src/features/querying/queries/flows.spec.ts +++ b/packages/riviere-builder/domain-model/src/domain/query/flows.spec.ts @@ -1,10 +1,11 @@ -import { RiviereQuery } from './RiviereQuery' import { - createMinimalValidGraph, + assertDefined, createAPIComponent, + createMinimalValidGraph, createUseCaseComponent, - assertDefined, -} from '../../../platform/__fixtures__/riviere-graph-fixtures' +} from './__fixtures__/riviere-graph-fixtures' +import { RiviereQuery } from './RiviereQuery' +import { SearchWithFlowOptions } from './search-with-flow-options' describe('RiviereQuery.flows()', () => { it('returns single flow when graph has one entry point', () => { @@ -91,9 +92,7 @@ describe('RiviereQuery.flows()', () => { }, ])( 'sets step $stepIndex to $expectedId with depth $expectedDepth', - ({ - stepIndex, expectedId, expectedDepth - }) => { + ({ stepIndex, expectedId, expectedDepth }) => { const query = new RiviereQuery(createLinkedGraph()) const result = query.flows() @@ -356,16 +355,23 @@ describe('RiviereQuery.searchWithFlow()', () => { ) const query = new RiviereQuery(graph) - const result = query.searchWithFlow('', { returnAllOnEmptyQuery: true }) + const result = query.searchWithFlow( + '', + SearchWithFlowOptions.parse({ returnAllOnEmptyQuery: true }), + ) - expect(result.matchingIds.slice().sort((a, b) => a.localeCompare(b))).toStrictEqual([ - 'test:api:a', - 'test:mod:ui:page', - ]) - expect(result.visibleIds.slice().sort((a, b) => a.localeCompare(b))).toStrictEqual([ - 'test:api:a', - 'test:mod:ui:page', - ]) + expect( + result.matchingIds + .slice() + .sort((a, b) => a.localeCompare(b)) + .map((id) => id.value), + ).toStrictEqual(['test:api:a', 'test:mod:ui:page']) + expect( + result.visibleIds + .slice() + .sort((a, b) => a.localeCompare(b)) + .map((id) => id.value), + ).toStrictEqual(['test:api:a', 'test:mod:ui:page']) }) it('returns empty arrays when query is empty and returnAllOnEmptyQuery is false', () => { @@ -379,10 +385,13 @@ describe('RiviereQuery.searchWithFlow()', () => { ) const query = new RiviereQuery(graph) - const result = query.searchWithFlow('', { returnAllOnEmptyQuery: false }) + const result = query.searchWithFlow( + '', + SearchWithFlowOptions.parse({ returnAllOnEmptyQuery: false }), + ) - expect(result.matchingIds).toStrictEqual([]) - expect(result.visibleIds).toStrictEqual([]) + expect(result.matchingIds.map((id) => id.value)).toStrictEqual([]) + expect(result.visibleIds.map((id) => id.value)).toStrictEqual([]) }) it('returns matching component ID and all connected component IDs as visible', () => { @@ -411,22 +420,29 @@ describe('RiviereQuery.searchWithFlow()', () => { ] const query = new RiviereQuery(graph) - const result = query.searchWithFlow('API A', { returnAllOnEmptyQuery: false }) + const result = query.searchWithFlow( + 'API A', + SearchWithFlowOptions.parse({ returnAllOnEmptyQuery: false }), + ) - expect(result.matchingIds).toStrictEqual(['test:api:a']) - expect(result.visibleIds.slice().sort((a, b) => a.localeCompare(b))).toStrictEqual([ - 'test:api:a', - 'test:mod:ui:page', - 'test:uc:b', - ]) + expect(result.matchingIds.map((id) => id.value)).toStrictEqual(['test:api:a']) + expect( + result.visibleIds + .slice() + .sort((a, b) => a.localeCompare(b)) + .map((id) => id.value), + ).toStrictEqual(['test:api:a', 'test:mod:ui:page', 'test:uc:b']) }) it('returns empty arrays when query matches nothing', () => { const query = new RiviereQuery(createMinimalValidGraph()) - const result = query.searchWithFlow('nonexistent', { returnAllOnEmptyQuery: false }) + const result = query.searchWithFlow( + 'nonexistent', + SearchWithFlowOptions.parse({ returnAllOnEmptyQuery: false }), + ) - expect(result.matchingIds).toStrictEqual([]) - expect(result.visibleIds).toStrictEqual([]) + expect(result.matchingIds.map((id) => id.value)).toStrictEqual([]) + expect(result.visibleIds.map((id) => id.value)).toStrictEqual([]) }) }) diff --git a/packages/riviere-builder/domain-model/src/domain/query/graph-diff.ts b/packages/riviere-builder/domain-model/src/domain/query/graph-diff.ts new file mode 100644 index 000000000..41ae71556 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/graph-diff.ts @@ -0,0 +1,123 @@ +import type { + Component, + Link, + RiviereGraph, +} from '@living-architecture/riviere-schema-published-language/schema' +import { ComponentId } from './component-id' +import { ComponentModification } from './component-modification' +import { DiffStats } from './diff-stats' +import { createLinkKey } from './link-key' + +/** @riviere-role value-object */ +export class GraphDiff { + declare private readonly brand: 'GraphDiff' + readonly components: { + added: Component[] + removed: Component[] + modified: ComponentModification[] + } + readonly links: { + added: Link[] + removed: Link[] + } + readonly stats: DiffStats + + private constructor(input: { + readonly components: { + added: Component[] + removed: Component[] + modified: ComponentModification[] + } + readonly links: { + added: Link[] + removed: Link[] + } + readonly stats: DiffStats + }) { + this.components = input.components + this.links = input.links + this.stats = input.stats + } + + static parse(input: { + readonly components: { + added: Component[] + removed: Component[] + modified: ComponentModification[] + } + readonly links: { + added: Link[] + removed: Link[] + } + readonly stats: DiffStats + }): GraphDiff { + return new GraphDiff(input) + } +} + +/** @riviere-role domain-service */ +export function diffGraphs(current: RiviereGraph, other: RiviereGraph): GraphDiff { + const thisIds = new Set(current.components.map((c) => c.id)) + const otherIds = new Set(other.components.map((c) => c.id)) + const otherById = new Map(other.components.map((c) => [c.id, c])) + + const added = other.components.filter((c) => !thisIds.has(c.id)) + const removed = current.components.filter((c) => !otherIds.has(c.id)) + const modified: ComponentModification[] = [] + + for (const tc of current.components) { + const oc = otherById.get(tc.id) + if (oc === undefined) continue + const changedFields = findChangedFields(tc, oc) + if (changedFields.length > 0) { + modified.push( + ComponentModification.parse({ + id: ComponentId.parse(tc.id), + before: tc, + after: oc, + changedFields, + }), + ) + } + } + + const thisLinkKeys = new Set(current.links.map((l) => createLinkKey(l).value)) + const otherLinkKeys = new Set(other.links.map((l) => createLinkKey(l).value)) + const linksAdded = other.links.filter((l) => !thisLinkKeys.has(createLinkKey(l).value)) + const linksRemoved = current.links.filter((l) => !otherLinkKeys.has(createLinkKey(l).value)) + + return GraphDiff.parse({ + components: { + added, + removed, + modified, + }, + links: { + added: linksAdded, + removed: linksRemoved, + }, + stats: DiffStats.parse({ + componentsAdded: added.length, + componentsRemoved: removed.length, + componentsModified: modified.length, + linksAdded: linksAdded.length, + linksRemoved: linksRemoved.length, + }), + }) +} + +function findChangedFields(before: Component, after: Component): string[] { + const beforeEntries = new Map(Object.entries(before)) + const afterEntries = new Map(Object.entries(after)) + const changedFields: string[] = [] + const allKeys = new Set([...beforeEntries.keys(), ...afterEntries.keys()]) + + for (const key of allKeys) { + if (key === 'id') continue + if (JSON.stringify(beforeEntries.get(key)) !== JSON.stringify(afterEntries.get(key))) { + changedFields.push(key) + } + } + + return changedFields +} diff --git a/packages/riviere-builder/domain-model/src/domain/query/graph-stats.ts b/packages/riviere-builder/domain-model/src/domain/query/graph-stats.ts new file mode 100644 index 000000000..af9ee083b --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/graph-stats.ts @@ -0,0 +1,37 @@ +/** @riviere-role value-object */ +export class GraphStats { + declare private readonly brand: 'GraphStats' + readonly componentCount: number + readonly linkCount: number + readonly domainCount: number + readonly apiCount: number + readonly entityCount: number + readonly eventCount: number + + private constructor(input: { + readonly componentCount: number + readonly linkCount: number + readonly domainCount: number + readonly apiCount: number + readonly entityCount: number + readonly eventCount: number + }) { + this.componentCount = input.componentCount + this.linkCount = input.linkCount + this.domainCount = input.domainCount + this.apiCount = input.apiCount + this.entityCount = input.entityCount + this.eventCount = input.eventCount + } + + static parse(input: { + readonly componentCount: number + readonly linkCount: number + readonly domainCount: number + readonly apiCount: number + readonly entityCount: number + readonly eventCount: number + }): GraphStats { + return new GraphStats(input) + } +} diff --git a/packages/riviere-builder/domain-model/src/domain/query/graph-validation.ts b/packages/riviere-builder/domain-model/src/domain/query/graph-validation.ts new file mode 100644 index 000000000..5569cb885 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/graph-validation.ts @@ -0,0 +1,15 @@ +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' +import { ComponentId } from './component-id' + +/** @riviere-role domain-service */ +export function detectOrphanComponents(graph: RiviereGraph): ComponentId[] { + const connectedComponentIds = new Set() + graph.links.forEach((link) => { + connectedComponentIds.add(link.source) + connectedComponentIds.add(link.target) + }) + + return graph.components + .filter((component) => !connectedComponentIds.has(component.id)) + .map((component) => ComponentId.parse(component.id)) +} diff --git a/packages/riviere-builder/domain-model/src/domain/query/handler-id.ts b/packages/riviere-builder/domain-model/src/domain/query/handler-id.ts new file mode 100644 index 000000000..93c96b73d --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/handler-id.ts @@ -0,0 +1,18 @@ +import { z } from 'zod' + +const schema = z.string() + +/** @riviere-role value-object */ +export class HandlerId { + declare private readonly brand: 'HandlerId' + + private constructor(readonly value: string) {} + + static parse(value: string): HandlerId { + return new HandlerId(schema.parse(value)) + } + + toJSON(): string { + return this.value + } +} diff --git a/packages/riviere-builder/domain-model/src/domain/query/handler-name.ts b/packages/riviere-builder/domain-model/src/domain/query/handler-name.ts new file mode 100644 index 000000000..0c18d75ea --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/handler-name.ts @@ -0,0 +1,18 @@ +import { z } from 'zod' + +const schema = z.string() + +/** @riviere-role value-object */ +export class HandlerName { + declare private readonly brand: 'HandlerName' + + private constructor(readonly value: string) {} + + static parse(value: string): HandlerName { + return new HandlerName(schema.parse(value)) + } + + toJSON(): string { + return this.value + } +} diff --git a/packages/riviere-builder/domain-model/src/domain/query/identifier-serialization.spec.ts b/packages/riviere-builder/domain-model/src/domain/query/identifier-serialization.spec.ts new file mode 100644 index 000000000..34cf63c49 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/identifier-serialization.spec.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from 'vitest' +import { ComponentId } from './component-id' +import { LinkId } from './link-id' + +describe('identifier serialization', () => { + it('serializes a component ID to its value', () => { + expect(JSON.stringify(ComponentId.parse('component-id'))).toBe('"component-id"') + }) + + it('serializes a link ID to its value', () => { + expect(JSON.stringify(LinkId.parse('link-id'))).toBe('"link-id"') + }) +}) diff --git a/packages/riviere-builder/domain-model/src/domain/query/known-source-event.ts b/packages/riviere-builder/domain-model/src/domain/query/known-source-event.ts new file mode 100644 index 000000000..cb4e8cf88 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/known-source-event.ts @@ -0,0 +1,31 @@ +import type { DomainName } from './domain-name' +import type { EventName } from './event-name' + +/** + * A subscribed event where the source domain is known. + * @riviere-role value-object + */ +export class KnownSourceEvent { + declare private readonly brand: 'KnownSourceEvent' + readonly eventName: EventName + readonly sourceDomain: DomainName + readonly sourceKnown: true + + private constructor(input: { + readonly eventName: EventName + readonly sourceDomain: DomainName + readonly sourceKnown: true + }) { + this.eventName = input.eventName + this.sourceDomain = input.sourceDomain + this.sourceKnown = input.sourceKnown + } + + static parse(input: { + readonly eventName: EventName + readonly sourceDomain: DomainName + readonly sourceKnown: true + }): KnownSourceEvent { + return new KnownSourceEvent(input) + } +} diff --git a/packages/riviere-builder/domain-model/src/domain/query/link-id.ts b/packages/riviere-builder/domain-model/src/domain/query/link-id.ts new file mode 100644 index 000000000..ae53f0a0d --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/link-id.ts @@ -0,0 +1,22 @@ +import { z } from 'zod' + +const schema = z.string() + +/** @riviere-role value-object */ +export class LinkId { + declare private readonly brand: 'LinkId' + + private constructor(readonly value: string) {} + + static parse(value: string): LinkId { + return new LinkId(schema.parse(value)) + } + + localeCompare(other: LinkId): number { + return this.value.localeCompare(other.value) + } + + toJSON(): string { + return this.value + } +} diff --git a/packages/riviere-builder/domain-model/src/domain/query/link-key.ts b/packages/riviere-builder/domain-model/src/domain/query/link-key.ts new file mode 100644 index 000000000..278d254db --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/link-key.ts @@ -0,0 +1,10 @@ +import type { Link } from '@living-architecture/riviere-schema-published-language/schema' +import { LinkId } from './link-id' + +/** @riviere-role domain-service */ +export function createLinkKey(link: Link): LinkId { + if (link.id !== undefined) { + return LinkId.parse(link.id) + } + return LinkId.parse(`${link.source}->${link.target}`) +} diff --git a/packages/riviere-query/src/features/querying/queries/nodeDepths.spec.ts b/packages/riviere-builder/domain-model/src/domain/query/nodeDepths.spec.ts similarity index 77% rename from packages/riviere-query/src/features/querying/queries/nodeDepths.spec.ts rename to packages/riviere-builder/domain-model/src/domain/query/nodeDepths.spec.ts index 535eb7eeb..a83739ead 100644 --- a/packages/riviere-query/src/features/querying/queries/nodeDepths.spec.ts +++ b/packages/riviere-builder/domain-model/src/domain/query/nodeDepths.spec.ts @@ -1,16 +1,13 @@ +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' +import { describe, expect, it } from 'vitest' import { - describe, it, expect -} from 'vitest' -import { - RiviereQuery, parseComponentId -} from './RiviereQuery' -import { - createMinimalValidGraph, createAPIComponent, + createMinimalValidGraph, createUseCaseComponent, defaultSourceLocation, -} from '../../../platform/__fixtures__/riviere-graph-fixtures' -import type { RiviereGraph } from '@living-architecture/riviere-schema' +} from './__fixtures__/riviere-graph-fixtures' +import { RiviereQuery } from './RiviereQuery' +import { ComponentId } from './component-id' describe('nodeDepths', () => { it('returns depth 0 for entry points', () => { @@ -19,7 +16,7 @@ describe('nodeDepths', () => { const query = new RiviereQuery(graph) const depths = query.nodeDepths() - expect(depths.get(parseComponentId('test:mod:ui:page'))).toBe(0) + expect(depths.get(ComponentId.parse('test:mod:ui:page'))).toBe(0) }) it('returns depth based on hops from entry point', () => { @@ -50,9 +47,9 @@ describe('nodeDepths', () => { const query = new RiviereQuery(graph) const depths = query.nodeDepths() - expect(depths.get(parseComponentId('test:mod:ui:page'))).toBe(0) - expect(depths.get(parseComponentId('test:mod:api:users'))).toBe(1) - expect(depths.get(parseComponentId('test:mod:uc:getUser'))).toBe(2) + expect(depths.get(ComponentId.parse('test:mod:ui:page'))).toBe(0) + expect(depths.get(ComponentId.parse('test:mod:api:users'))).toBe(1) + expect(depths.get(ComponentId.parse('test:mod:uc:getUser'))).toBe(2) }) it('returns minimum depth when reachable from multiple entry points', () => { @@ -83,9 +80,9 @@ describe('nodeDepths', () => { const query = new RiviereQuery(graph) const depths = query.nodeDepths() - expect(depths.get(parseComponentId('test:mod:ui:page'))).toBe(0) - expect(depths.get(parseComponentId('test:mod:api:direct'))).toBe(0) - expect(depths.get(parseComponentId('test:mod:uc:shared'))).toBe(1) + expect(depths.get(ComponentId.parse('test:mod:ui:page'))).toBe(0) + expect(depths.get(ComponentId.parse('test:mod:api:direct'))).toBe(0) + expect(depths.get(ComponentId.parse('test:mod:uc:shared'))).toBe(1) }) it('excludes unreachable nodes from result', () => { @@ -101,8 +98,8 @@ describe('nodeDepths', () => { const query = new RiviereQuery(graph) const depths = query.nodeDepths() - expect(depths.get(parseComponentId('test:mod:ui:page'))).toBe(0) - expect(depths.has(parseComponentId('test:mod:uc:orphan'))).toBe(false) + expect(depths.get(ComponentId.parse('test:mod:ui:page'))).toBe(0) + expect(depths.has(ComponentId.parse('test:mod:uc:orphan'))).toBe(false) }) it('handles source with multiple outgoing links', () => { @@ -133,9 +130,9 @@ describe('nodeDepths', () => { const query = new RiviereQuery(graph) const depths = query.nodeDepths() - expect(depths.get(parseComponentId('test:mod:ui:page'))).toBe(0) - expect(depths.get(parseComponentId('test:mod:uc:a'))).toBe(1) - expect(depths.get(parseComponentId('test:mod:uc:b'))).toBe(1) + expect(depths.get(ComponentId.parse('test:mod:ui:page'))).toBe(0) + expect(depths.get(ComponentId.parse('test:mod:uc:a'))).toBe(1) + expect(depths.get(ComponentId.parse('test:mod:uc:b'))).toBe(1) }) it('returns empty map for graph with no entry points', () => { diff --git a/packages/riviere-builder/domain-model/src/domain/query/operation-name.ts b/packages/riviere-builder/domain-model/src/domain/query/operation-name.ts new file mode 100644 index 000000000..142e9e361 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/operation-name.ts @@ -0,0 +1,18 @@ +import { z } from 'zod' + +const schema = z.string() + +/** @riviere-role value-object */ +export class OperationName { + declare private readonly brand: 'OperationName' + + private constructor(readonly value: string) {} + + static parse(value: string): OperationName { + return new OperationName(schema.parse(value)) + } + + toJSON(): string { + return this.value + } +} diff --git a/packages/riviere-builder/domain-model/src/domain/query/published-event.ts b/packages/riviere-builder/domain-model/src/domain/query/published-event.ts new file mode 100644 index 000000000..21baf2591 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/published-event.ts @@ -0,0 +1,37 @@ +import type { DomainName } from './domain-name' +import type { EventId } from './event-id' +import type { EventName } from './event-name' +import { EventSubscriber } from './event-subscriber' + +/** + * A published event with its subscribers. + * @riviere-role value-object + */ +export class PublishedEvent { + declare private readonly brand: 'PublishedEvent' + readonly id: EventId + readonly eventName: EventName + readonly domain: DomainName + readonly handlers: EventSubscriber[] + + private constructor(input: { + readonly id: EventId + readonly eventName: EventName + readonly domain: DomainName + readonly handlers: EventSubscriber[] + }) { + this.id = input.id + this.eventName = input.eventName + this.domain = input.domain + this.handlers = input.handlers + } + + static parse(input: { + readonly id: EventId + readonly eventName: EventName + readonly domain: DomainName + readonly handlers: EventSubscriber[] + }): PublishedEvent { + return new PublishedEvent(input) + } +} diff --git a/packages/riviere-builder/domain-model/src/domain/query/search-with-flow-options.ts b/packages/riviere-builder/domain-model/src/domain/query/search-with-flow-options.ts new file mode 100644 index 000000000..09b1cbdb6 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/search-with-flow-options.ts @@ -0,0 +1,13 @@ +/** @riviere-role value-object */ +export class SearchWithFlowOptions { + declare private readonly brand: 'SearchWithFlowOptions' + readonly returnAllOnEmptyQuery: boolean + + private constructor(input: { readonly returnAllOnEmptyQuery: boolean }) { + this.returnAllOnEmptyQuery = input.returnAllOnEmptyQuery + } + + static parse(input: { readonly returnAllOnEmptyQuery: boolean }): SearchWithFlowOptions { + return new SearchWithFlowOptions(input) + } +} diff --git a/packages/riviere-builder/domain-model/src/domain/query/search-with-flow-result.ts b/packages/riviere-builder/domain-model/src/domain/query/search-with-flow-result.ts new file mode 100644 index 000000000..4c0d1f95e --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/search-with-flow-result.ts @@ -0,0 +1,23 @@ +import { ComponentId } from './component-id' + +/** @riviere-role value-object */ +export class SearchWithFlowResult { + declare private readonly brand: 'SearchWithFlowResult' + readonly matchingIds: ComponentId[] + readonly visibleIds: ComponentId[] + + private constructor(input: { + readonly matchingIds: ComponentId[] + readonly visibleIds: ComponentId[] + }) { + this.matchingIds = input.matchingIds + this.visibleIds = input.visibleIds + } + + static parse(input: { + readonly matchingIds: ComponentId[] + readonly visibleIds: ComponentId[] + }): SearchWithFlowResult { + return new SearchWithFlowResult(input) + } +} diff --git a/packages/riviere-query/src/features/querying/queries/state-machine.spec.ts b/packages/riviere-builder/domain-model/src/domain/query/state-machine.spec.ts similarity index 88% rename from packages/riviere-query/src/features/querying/queries/state-machine.spec.ts rename to packages/riviere-builder/domain-model/src/domain/query/state-machine.spec.ts index fb3594a95..3fa4da9bf 100644 --- a/packages/riviere-query/src/features/querying/queries/state-machine.spec.ts +++ b/packages/riviere-builder/domain-model/src/domain/query/state-machine.spec.ts @@ -1,8 +1,8 @@ -import { RiviereQuery } from './RiviereQuery' import { - createMinimalValidGraph, createDomainOpComponent, -} from '../../../platform/__fixtures__/riviere-graph-fixtures' + createMinimalValidGraph, +} from './__fixtures__/riviere-graph-fixtures' +import { RiviereQuery } from './RiviereQuery' describe('transitionsFor', () => { it('returns empty array for nonexistent entity but transitions for existing entity', () => { @@ -65,7 +65,7 @@ describe('transitionsFor', () => { const transitions = query.transitionsFor('Order') - expect(transitions).toStrictEqual([ + expect(JSON.parse(JSON.stringify(transitions))).toStrictEqual([ { from: 'Draft', to: 'Placed', @@ -100,7 +100,7 @@ describe('transitionsFor', () => { const transitions = query.transitionsFor('Order') - expect(transitions).toStrictEqual([ + expect(JSON.parse(JSON.stringify(transitions))).toStrictEqual([ { from: '*', to: 'Cancelled', @@ -137,7 +137,7 @@ describe('transitionsFor', () => { const transitions = query.transitionsFor('Order') - expect(transitions).toStrictEqual([ + expect(JSON.parse(JSON.stringify(transitions))).toStrictEqual([ { from: 'Draft', to: 'Placed', @@ -208,10 +208,10 @@ describe('statesFor', () => { const states = query.statesFor('Order') - expect(states).toHaveLength(3) - expect(states).toContain('Draft') - expect(states).toContain('Placed') - expect(states).toContain('Confirmed') + expect(states.map((state) => state.value)).toHaveLength(3) + expect(states.map((state) => state.value)).toContain('Draft') + expect(states.map((state) => state.value)).toContain('Placed') + expect(states.map((state) => state.value)).toContain('Confirmed') }) it('orders states by transition flow from initial to terminal', () => { @@ -261,7 +261,12 @@ describe('statesFor', () => { const states = query.statesFor('Order') - expect(states).toStrictEqual(['Draft', 'Placed', 'Confirmed', 'Shipped']) + expect(states.map((state) => state.value)).toStrictEqual([ + 'Draft', + 'Placed', + 'Confirmed', + 'Shipped', + ]) }) it('excludes wildcard from states but includes target states', () => { @@ -298,8 +303,8 @@ describe('statesFor', () => { const states = query.statesFor('Order') - expect(states).not.toContain('*') - expect(states).toContain('Cancelled') + expect(states.map((state) => state.value)).not.toContain('*') + expect(states.map((state) => state.value)).toContain('Cancelled') }) it('ignores operations without stateChanges when ordering', () => { @@ -330,7 +335,7 @@ describe('statesFor', () => { const states = query.statesFor('Order') - expect(states).toStrictEqual(['Draft', 'Placed']) + expect(states.map((state) => state.value)).toStrictEqual(['Draft', 'Placed']) }) it('handles cycles in state transitions', () => { @@ -367,9 +372,9 @@ describe('statesFor', () => { const states = query.statesFor('Order') - expect(states).toHaveLength(2) - expect(states).toContain('Draft') - expect(states).toContain('Active') + expect(states.map((state) => state.value)).toHaveLength(2) + expect(states.map((state) => state.value)).toContain('Draft') + expect(states.map((state) => state.value)).toContain('Active') }) it('handles cycles with initial state', () => { @@ -419,9 +424,9 @@ describe('statesFor', () => { const states = query.statesFor('Order') - expect(states[0]).toBe('Draft') - expect(states).toContain('Active') - expect(states).toContain('Processing') - expect(states).toHaveLength(3) + expect(states[0]?.value).toBe('Draft') + expect(states.map((state) => state.value)).toContain('Active') + expect(states.map((state) => state.value)).toContain('Processing') + expect(states.map((state) => state.value)).toHaveLength(3) }) }) diff --git a/packages/riviere-builder/domain-model/src/domain/query/state.ts b/packages/riviere-builder/domain-model/src/domain/query/state.ts new file mode 100644 index 000000000..7f68328b9 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/state.ts @@ -0,0 +1,18 @@ +import { z } from 'zod' + +const schema = z.string() + +/** @riviere-role value-object */ +export class State { + declare private readonly brand: 'State' + + private constructor(readonly value: string) {} + + static parse(value: string): State { + return new State(schema.parse(value)) + } + + toJSON(): string { + return this.value + } +} diff --git a/packages/riviere-builder/domain-model/src/domain/query/stats-queries.ts b/packages/riviere-builder/domain-model/src/domain/query/stats-queries.ts new file mode 100644 index 000000000..4e791c612 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/stats-queries.ts @@ -0,0 +1,24 @@ +import type { + DomainOpComponent, + RiviereGraph, +} from '@living-architecture/riviere-schema-published-language/schema' +import { GraphStats } from './graph-stats' + +/** @riviere-role domain-service */ +export function queryStats(graph: RiviereGraph): GraphStats { + const components = graph.components + + const uniqueDomains = new Set(components.map((c) => c.domain)) + + const domainOps = components.filter((c): c is DomainOpComponent => c.type === 'DomainOp') + const uniqueEntities = new Set(domainOps.filter((c) => c.entity).map((c) => c.entity)) + + return GraphStats.parse({ + componentCount: components.length, + linkCount: graph.links.length, + domainCount: uniqueDomains.size, + apiCount: components.filter((c) => c.type === 'API').length, + entityCount: uniqueEntities.size, + eventCount: components.filter((c) => c.type === 'Event').length, + }) +} diff --git a/packages/riviere-query/src/features/querying/queries/stats.spec.ts b/packages/riviere-builder/domain-model/src/domain/query/stats.spec.ts similarity index 96% rename from packages/riviere-query/src/features/querying/queries/stats.spec.ts rename to packages/riviere-builder/domain-model/src/domain/query/stats.spec.ts index 6876f424d..37324ea4f 100644 --- a/packages/riviere-query/src/features/querying/queries/stats.spec.ts +++ b/packages/riviere-builder/domain-model/src/domain/query/stats.spec.ts @@ -1,15 +1,13 @@ +import type { RiviereGraph } from '@living-architecture/riviere-schema-published-language/schema' +import { describe, expect, it } from 'vitest' import { - describe, it, expect -} from 'vitest' -import { RiviereQuery } from './RiviereQuery' -import type { RiviereGraph } from '@living-architecture/riviere-schema' -import { - createMinimalValidGraph, createAPIComponent, - createEventComponent, createDomainOpComponent, + createEventComponent, + createMinimalValidGraph, defaultSourceLocation, -} from '../../../platform/__fixtures__/riviere-graph-fixtures' +} from './__fixtures__/riviere-graph-fixtures' +import { RiviereQuery } from './RiviereQuery' describe('stats', () => { it('returns componentCount matching number of components', () => { @@ -201,7 +199,7 @@ describe('stats', () => { const query = new RiviereQuery(graph) const result = query.stats() - expect(result).toStrictEqual({ + expect(JSON.parse(JSON.stringify(result))).toStrictEqual({ componentCount: 0, linkCount: 0, domainCount: 0, diff --git a/packages/riviere-builder/domain-model/src/domain/query/traceFlow.spec.ts b/packages/riviere-builder/domain-model/src/domain/query/traceFlow.spec.ts new file mode 100644 index 000000000..d5c940c87 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/traceFlow.spec.ts @@ -0,0 +1,347 @@ +import { createAPIComponent, createMinimalValidGraph } from './__fixtures__/riviere-graph-fixtures' +import { ComponentNotFoundError, RiviereQuery } from './RiviereQuery' +import { ComponentId } from './component-id' + +describe('RiviereQuery.traceFlow()', () => { + it('throws ComponentNotFoundError when startComponentId does not exist', () => { + const query = new RiviereQuery(createMinimalValidGraph()) + + expect(() => query.traceFlow(ComponentId.parse('nonexistent:mod:api:foo'))).toThrow( + ComponentNotFoundError, + ) + }) + + it('includes componentId in ComponentNotFoundError', () => { + const query = new RiviereQuery(createMinimalValidGraph()) + + const captureError = (): ComponentNotFoundError | undefined => { + try { + query.traceFlow(ComponentId.parse('nonexistent:mod:api:foo')) + return undefined + } catch (error) { + if (error instanceof ComponentNotFoundError) { + return error + } + return undefined + } + } + + const caughtError = captureError() + expect(caughtError).toBeDefined() + expect(caughtError?.componentId).toBe('nonexistent:mod:api:foo') + }) + + it('returns only starting component when component is isolated', () => { + const query = new RiviereQuery(createMinimalValidGraph()) + + const result = query.traceFlow(ComponentId.parse('test:mod:ui:page')) + + expect(result.componentIds.map((id) => id.value)).toStrictEqual(['test:mod:ui:page']) + expect(result.linkIds.map((id) => id.value)).toStrictEqual([]) + }) + + it('returns downstream components when starting from source', () => { + const graph = createMinimalValidGraph() + graph.components.push( + createAPIComponent({ + id: 'test:api:create', + name: 'Create', + domain: 'test', + }), + ) + graph.links = [ + { + source: 'test:mod:ui:page', + target: 'test:api:create', + }, + ] + const query = new RiviereQuery(graph) + + const result = query.traceFlow(ComponentId.parse('test:mod:ui:page')) + + expect( + result.componentIds + .slice() + .sort((a, b) => a.localeCompare(b)) + .map((id) => id.value), + ).toStrictEqual(['test:api:create', 'test:mod:ui:page']) + expect(result.linkIds.map((id) => id.value)).toStrictEqual([ + 'test:mod:ui:page->test:api:create', + ]) + }) + + it('returns upstream components when starting from target', () => { + const graph = createMinimalValidGraph() + graph.components.push( + createAPIComponent({ + id: 'test:api:create', + name: 'Create', + domain: 'test', + }), + ) + graph.links = [ + { + source: 'test:mod:ui:page', + target: 'test:api:create', + }, + ] + const query = new RiviereQuery(graph) + + const result = query.traceFlow(ComponentId.parse('test:api:create')) + + expect( + result.componentIds + .slice() + .sort((a, b) => a.localeCompare(b)) + .map((id) => id.value), + ).toStrictEqual(['test:api:create', 'test:mod:ui:page']) + expect(result.linkIds.map((id) => id.value)).toStrictEqual([ + 'test:mod:ui:page->test:api:create', + ]) + }) + + it('returns all branches when flow branches', () => { + const graph = createMinimalValidGraph() + graph.components.push( + createAPIComponent({ + id: 'test:api:a', + name: 'API A', + domain: 'test', + }), + createAPIComponent({ + id: 'test:api:b', + name: 'API B', + domain: 'test', + }), + ) + graph.links = [ + { + source: 'test:mod:ui:page', + target: 'test:api:a', + }, + { + source: 'test:mod:ui:page', + target: 'test:api:b', + }, + ] + const query = new RiviereQuery(graph) + + const result = query.traceFlow(ComponentId.parse('test:mod:ui:page')) + + expect( + result.componentIds + .slice() + .sort((a, b) => a.localeCompare(b)) + .map((id) => id.value), + ).toStrictEqual(['test:api:a', 'test:api:b', 'test:mod:ui:page']) + expect( + result.linkIds + .slice() + .sort((a, b) => a.localeCompare(b)) + .map((id) => id.value), + ).toStrictEqual(['test:mod:ui:page->test:api:a', 'test:mod:ui:page->test:api:b']) + }) + + it('handles cycles without infinite loop', () => { + const graph = createMinimalValidGraph() + graph.components.push( + createAPIComponent({ + id: 'test:api:a', + name: 'API A', + domain: 'test', + }), + ) + graph.links = [ + { + source: 'test:mod:ui:page', + target: 'test:api:a', + }, + { + source: 'test:api:a', + target: 'test:mod:ui:page', + }, + ] + const query = new RiviereQuery(graph) + + const result = query.traceFlow(ComponentId.parse('test:mod:ui:page')) + + expect( + result.componentIds + .slice() + .sort((a, b) => a.localeCompare(b)) + .map((id) => id.value), + ).toStrictEqual(['test:api:a', 'test:mod:ui:page']) + expect( + result.linkIds + .slice() + .sort((a, b) => a.localeCompare(b)) + .map((id) => id.value), + ).toStrictEqual(['test:api:a->test:mod:ui:page', 'test:mod:ui:page->test:api:a']) + }) + + it('only includes components in connected subgraph', () => { + const graph = createMinimalValidGraph() + graph.metadata.domains['other'] = { + description: 'Other', + systemType: 'domain', + } + graph.components.push( + createAPIComponent({ + id: 'test:api:a', + name: 'API A', + domain: 'test', + }), + createAPIComponent({ + id: 'other:api:x', + name: 'API X', + domain: 'other', + }), + createAPIComponent({ + id: 'other:api:y', + name: 'API Y', + domain: 'other', + }), + ) + graph.links = [ + { + source: 'test:mod:ui:page', + target: 'test:api:a', + }, + { + source: 'other:api:x', + target: 'other:api:y', + }, + ] + const query = new RiviereQuery(graph) + + const result = query.traceFlow(ComponentId.parse('test:mod:ui:page')) + + expect( + result.componentIds + .slice() + .sort((a, b) => a.localeCompare(b)) + .map((id) => id.value), + ).toStrictEqual(['test:api:a', 'test:mod:ui:page']) + expect(result.linkIds.map((id) => id.value)).toStrictEqual(['test:mod:ui:page->test:api:a']) + }) + + it('traces full chain when starting from middle', () => { + const graph = createMinimalValidGraph() + graph.components.push( + createAPIComponent({ + id: 'test:api:b', + name: 'API B', + domain: 'test', + }), + createAPIComponent({ + id: 'test:api:c', + name: 'API C', + domain: 'test', + }), + ) + graph.links = [ + { + source: 'test:mod:ui:page', + target: 'test:api:b', + }, + { + source: 'test:api:b', + target: 'test:api:c', + }, + ] + const query = new RiviereQuery(graph) + + const result = query.traceFlow(ComponentId.parse('test:api:b')) + + expect( + result.componentIds + .slice() + .sort((a, b) => a.localeCompare(b)) + .map((id) => id.value), + ).toStrictEqual(['test:api:b', 'test:api:c', 'test:mod:ui:page']) + expect( + result.linkIds + .slice() + .sort((a, b) => a.localeCompare(b)) + .map((id) => id.value), + ).toStrictEqual(['test:api:b->test:api:c', 'test:mod:ui:page->test:api:b']) + }) + + it('uses explicit link ID when provided', () => { + const graph = createMinimalValidGraph() + graph.components.push( + createAPIComponent({ + id: 'test:api:a', + name: 'API A', + domain: 'test', + }), + ) + graph.links = [ + { + id: 'custom-link-id', + source: 'test:mod:ui:page', + target: 'test:api:a', + }, + ] + const query = new RiviereQuery(graph) + + const result = query.traceFlow(ComponentId.parse('test:mod:ui:page')) + + expect(result.linkIds.map((id) => id.value)).toStrictEqual(['custom-link-id']) + }) + + it('traverses full chain beyond immediate neighbors', () => { + const graph = createMinimalValidGraph() + graph.components.push( + createAPIComponent({ + id: 'test:api:b', + name: 'API B', + domain: 'test', + }), + createAPIComponent({ + id: 'test:api:c', + name: 'API C', + domain: 'test', + }), + createAPIComponent({ + id: 'test:api:d', + name: 'API D', + domain: 'test', + }), + ) + graph.links = [ + { + source: 'test:mod:ui:page', + target: 'test:api:b', + }, + { + source: 'test:api:b', + target: 'test:api:c', + }, + { + source: 'test:api:c', + target: 'test:api:d', + }, + ] + const query = new RiviereQuery(graph) + + const result = query.traceFlow(ComponentId.parse('test:api:b')) + + expect( + result.componentIds + .slice() + .sort((a, b) => a.localeCompare(b)) + .map((id) => id.value), + ).toStrictEqual(['test:api:b', 'test:api:c', 'test:api:d', 'test:mod:ui:page']) + expect( + result.linkIds + .slice() + .sort((a, b) => a.localeCompare(b)) + .map((id) => id.value), + ).toStrictEqual([ + 'test:api:b->test:api:c', + 'test:api:c->test:api:d', + 'test:mod:ui:page->test:api:b', + ]) + }) +}) diff --git a/packages/riviere-builder/domain-model/src/domain/query/unknown-source-event.ts b/packages/riviere-builder/domain-model/src/domain/query/unknown-source-event.ts new file mode 100644 index 000000000..32d7ae1f3 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/query/unknown-source-event.ts @@ -0,0 +1,23 @@ +import type { EventName } from './event-name' + +/** + * A subscribed event where the source domain is unknown. + * @riviere-role value-object + */ +export class UnknownSourceEvent { + declare private readonly brand: 'UnknownSourceEvent' + readonly eventName: EventName + readonly sourceKnown: false + + private constructor(input: { readonly eventName: EventName; readonly sourceKnown: false }) { + this.eventName = input.eventName + this.sourceKnown = input.sourceKnown + } + + static parse(input: { + readonly eventName: EventName + readonly sourceKnown: false + }): UnknownSourceEvent { + return new UnknownSourceEvent(input) + } +} diff --git a/packages/riviere-builder/domain-model/src/domain/riviere-builder.ts b/packages/riviere-builder/domain-model/src/domain/riviere-builder.ts new file mode 100644 index 000000000..1408081a8 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/riviere-builder.ts @@ -0,0 +1,155 @@ +import type { + DomainMetadata, + RiviereGraph, + SourceInfo, +} from '@living-architecture/riviere-schema-published-language/schema' +import { BuilderGraph } from './builder-graph' +import { GraphConstruction } from './construction/graph-construction' +import { GraphEnrichment } from './enrichment/graph-enrichment' +import { GraphLinking } from './linking/graph-linking' +import { GraphInspection } from './inspection/graph-inspection' +import { NearMatch } from './error-recovery/near-match' +import { + BuildValidationError, + InvalidGraphError, + MissingDomainsError, + MissingSourcesError, +} from './construction/construction-errors' +import { toRiviereGraph } from './inspection/inspection-functions' + +type ScalarOverwriteWarning = Readonly<{ + code: 'SCALAR_OVERWRITE' + message: string + componentId: string + field: string + oldValue: string | number | boolean + newValue: string | number | boolean +}> + +type DuplicateLinkWarning = Readonly<{ + code: 'DUPLICATE_LINK_SKIPPED' + message: string + source: string + target: string + linkType?: string + targetRepository?: string + targetName: string +}> + +type OperationWarning = ScalarOverwriteWarning | DuplicateLinkWarning + +/** @riviere-role domain-service */ +export class RiviereBuilder { + readonly graphPath: string + + private graph: BuilderGraph + private readonly operationWarnings: OperationWarning[] + + private constructor(graph: BuilderGraph, graphPath: string) { + this.graph = graph + this.graphPath = graphPath + this.operationWarnings = [] + } + + get construction(): GraphConstruction { + return new GraphConstruction( + this.graph, + (warning) => this.operationWarnings.push(warning), + (graph) => { + this.graph = graph + }, + ) + } + + get enrichment(): GraphEnrichment { + return new GraphEnrichment(this.graph, (graph) => { + this.graph = graph + }) + } + + get linking(): GraphLinking { + return new GraphLinking( + this.graph, + (warning) => this.operationWarnings.push(warning), + (graph) => { + this.graph = graph + }, + ) + } + + get inspection(): GraphInspection { + return new GraphInspection(this.graph, this.operationWarnings) + } + + get errorRecovery(): NearMatch { + return new NearMatch(this.graph) + } + + static resume(graph: RiviereGraph, graphPath = ''): RiviereBuilder { + if (!graph.metadata.sources || graph.metadata.sources.length === 0) { + throw new InvalidGraphError('missing sources') + } + + const builderGraph = BuilderGraph.parse({ + version: graph.version, + metadata: { + ...graph.metadata, + sources: graph.metadata.sources, + customTypes: graph.metadata.customTypes ?? {}, + relationshipTypes: graph.metadata.relationshipTypes ?? {}, + }, + components: graph.components, + links: graph.links, + externalLinks: graph.externalLinks ?? [], + }) + return new RiviereBuilder(builderGraph, graphPath) + } + + static new( + options: { + readonly name?: string + readonly description?: string + readonly sources: readonly SourceInfo[] + readonly domains: Readonly> + }, + graphPath = '', + ): RiviereBuilder { + if (options.sources.length === 0) { + throw new MissingSourcesError() + } + + if (Object.keys(options.domains).length === 0) { + throw new MissingDomainsError() + } + + const graph = BuilderGraph.parse({ + version: '1.0', + metadata: { + ...(options.name !== undefined && { name: options.name }), + ...(options.description !== undefined && { description: options.description }), + sources: [...options.sources], + domains: { ...options.domains }, + customTypes: {}, + relationshipTypes: {}, + }, + components: [], + links: [], + externalLinks: [], + }) + + return new RiviereBuilder(graph, graphPath) + } + + serialize(): string { + return JSON.stringify(this.graph, null, 2) + } + + build(): RiviereGraph { + const result = this.inspection.validate() + if (!result.valid) { + const messages = result.errors.map((e) => e.message) + throw new BuildValidationError(messages) + } + return toRiviereGraph(this.graph) + } +} diff --git a/packages/riviere-builder/domain-model/src/domain/subscribed-events.ts b/packages/riviere-builder/domain-model/src/domain/subscribed-events.ts new file mode 100644 index 000000000..f2ae15231 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/subscribed-events.ts @@ -0,0 +1,20 @@ +/** @riviere-role value-object */ +export class SubscribedEvents { + declare private readonly brand: 'SubscribedEvents' + + private constructor(readonly values: readonly string[]) {} + + static parse(value: string | undefined) { + const values = + value + ?.split(',') + .map((event) => event.trim()) + .filter(Boolean) ?? [] + return values.length === 0 + ? { + success: false as const, + message: '--subscribed-events is required for EventHandler component', + } + : { success: true as const, data: new SubscribedEvents(values) } + } +} diff --git a/packages/riviere-builder/domain-model/src/domain/system-type.spec.ts b/packages/riviere-builder/domain-model/src/domain/system-type.spec.ts new file mode 100644 index 000000000..c8a1a2993 --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/system-type.spec.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from 'vitest' +import { SystemType } from './system-type' + +describe('SystemType', () => { + it.each(['domain', 'bff', 'ui', 'external-service', 'other'])('parses %s', (value) => { + const result = SystemType.parse(value) + + expect(result.success).toBe(true) + expect(result.success && result.data.value).toBe(value) + }) + + it('returns the validation error for an unsupported system type', () => { + const result = SystemType.parse('backend') + + expect(result.success).toBe(false) + expect(!result.success && result.error.issues).not.toHaveLength(0) + }) +}) diff --git a/packages/riviere-builder/domain-model/src/domain/system-type.ts b/packages/riviere-builder/domain-model/src/domain/system-type.ts new file mode 100644 index 000000000..65f4b8e5e --- /dev/null +++ b/packages/riviere-builder/domain-model/src/domain/system-type.ts @@ -0,0 +1,24 @@ +import { z } from 'zod' + +const systemTypeSchema = z.enum(['domain', 'bff', 'ui', 'external-service', 'other']) +type SystemTypeValue = z.infer + +/** @riviere-role value-object */ +export class SystemType { + declare private readonly brand: 'SystemType' + readonly value: SystemTypeValue + + private constructor(value: SystemTypeValue) { + this.value = value + } + + static parse(value: string) { + const parsed = systemTypeSchema.safeParse(value) + return parsed.success + ? { + data: new SystemType(parsed.data), + success: true as const, + } + : parsed + } +} diff --git a/packages/riviere-builder/src/platform/domain/text-similarity/string-similarity.spec.ts b/packages/riviere-builder/domain-model/src/domain/text-similarity/string-similarity.spec.ts similarity index 91% rename from packages/riviere-builder/src/platform/domain/text-similarity/string-similarity.spec.ts rename to packages/riviere-builder/domain-model/src/domain/text-similarity/string-similarity.spec.ts index d21db3f3f..e7f846a06 100644 --- a/packages/riviere-builder/src/platform/domain/text-similarity/string-similarity.spec.ts +++ b/packages/riviere-builder/domain-model/src/domain/text-similarity/string-similarity.spec.ts @@ -1,9 +1,5 @@ -import { - describe, it, expect -} from 'vitest' -import { - levenshteinDistance, similarityScore -} from './string-similarity' +import { describe, it, expect } from 'vitest' +import { levenshteinDistance, similarityScore } from './string-similarity' describe('levenshteinDistance', () => { it.each([ diff --git a/packages/riviere-builder/src/platform/domain/text-similarity/string-similarity.ts b/packages/riviere-builder/domain-model/src/domain/text-similarity/string-similarity.ts similarity index 100% rename from packages/riviere-builder/src/platform/domain/text-similarity/string-similarity.ts rename to packages/riviere-builder/domain-model/src/domain/text-similarity/string-similarity.ts diff --git a/packages/riviere-builder/domain-model/src/index.ts b/packages/riviere-builder/domain-model/src/index.ts new file mode 100644 index 000000000..735190ddf --- /dev/null +++ b/packages/riviere-builder/domain-model/src/index.ts @@ -0,0 +1,23 @@ +export { RiviereBuilder } from './domain/builder-facade' +export { ComponentId } from '@living-architecture/riviere-schema-published-language/component-id' +export { + BuildValidationError, + ComponentNotFoundError, + ComponentTypeMismatchError, + CustomTypeAlreadyDefinedError, + CustomTypeNotFoundError, + DomainNotFoundError, + DuplicateComponentError, + DuplicateDomainError, + DuplicateLinkError, + InvalidGraphError, + MissingDomainsError, + MissingRequiredPropertiesError, + MissingSourcesError, + RelationshipTypeAlreadyDefinedError, + RelationshipTypeNotFoundError, + SourceConflictError, +} from './domain/construction/construction-errors' +export { InvalidEnrichmentTargetError } from './domain/enrichment/enrichment-errors' +export { findNearMatches } from './domain/error-recovery/component-suggestion' +export { RiviereQuery } from './domain/query/RiviereQuery' diff --git a/packages/riviere-builder/domain-model/tsconfig.json b/packages/riviere-builder/domain-model/tsconfig.json new file mode 100644 index 000000000..667a3463d --- /dev/null +++ b/packages/riviere-builder/domain-model/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/packages/riviere-builder/domain-model/tsconfig.lib.json b/packages/riviere-builder/domain-model/tsconfig.lib.json new file mode 100644 index 000000000..6abfe048d --- /dev/null +++ b/packages/riviere-builder/domain-model/tsconfig.lib.json @@ -0,0 +1,33 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": ".", + "rootDir": "src", + "outDir": "dist", + "tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo", + "emitDeclarationOnly": false, + "forceConsistentCasingInFileNames": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "references": [ + { + "path": "../../riviere-schema/published-language/tsconfig.lib.json" + } + ], + "exclude": [ + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/__fixtures__/**" + ] +} diff --git a/packages/riviere-builder/domain-model/tsconfig.spec.json b/packages/riviere-builder/domain-model/tsconfig.spec.json new file mode 100644 index 000000000..c98b0e87c --- /dev/null +++ b/packages/riviere-builder/domain-model/tsconfig.spec.json @@ -0,0 +1,35 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./out-tsc/vitest", + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ], + "forceConsistentCasingInFileNames": true + }, + "include": [ + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts", + "src/__fixtures__/**/*.ts" + ], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/packages/riviere-builder/domain-model/typedoc.json b/packages/riviere-builder/domain-model/typedoc.json new file mode 100644 index 000000000..9cee20c4a --- /dev/null +++ b/packages/riviere-builder/domain-model/typedoc.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "entryPoints": ["./src/index.ts"], + "out": "../../../apps/docs/reference/api/generated/riviere-builder", + "plugin": ["typedoc-plugin-markdown", "typedoc-plugin-frontmatter"], + "frontmatterGlobals": { + "pageClass": "reference" + }, + "name": "@living-architecture/riviere-builder-domain-model", + "readme": "none", + "excludePrivate": true, + "excludeProtected": true, + "excludeInternal": true, + "includeVersion": false, + "disableSources": false, + "sourceLinkTemplate": "https://github.com/NTCoding/living-architecture/blob/main/{path}#L{line}", + "tsconfig": "./tsconfig.lib.json", + "validation": { + "invalidLink": true, + "notExported": false + }, + "hidePageHeader": true, + "hideBreadcrumbs": true +} diff --git a/packages/riviere-builder/domain-model/vite.config.ts b/packages/riviere-builder/domain-model/vite.config.ts new file mode 100644 index 000000000..66caeabfa --- /dev/null +++ b/packages/riviere-builder/domain-model/vite.config.ts @@ -0,0 +1,20 @@ +/// +import { defineConfig } from 'vite' + +export default defineConfig(() => ({ + root: import.meta.dirname, + cacheDir: '../../../node_modules/.vite/packages/riviere-builder', + plugins: [], + test: { + name: '@living-architecture/riviere-builder-domain-model', + watch: false, + globals: true, + environment: 'node', + include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: './test-output/vitest/coverage', + provider: 'v8' as const, + }, + }, +})) diff --git a/packages/riviere-builder/domain-model/vitest.config.mts b/packages/riviere-builder/domain-model/vitest.config.mts new file mode 100644 index 000000000..5c1bf4466 --- /dev/null +++ b/packages/riviere-builder/domain-model/vitest.config.mts @@ -0,0 +1,32 @@ +import path from 'node:path' +import { defineConfig } from 'vitest/config' + +const repoRoot = path.resolve(__dirname, '../../..') + +export default defineConfig(() => ({ + root: __dirname, + cacheDir: '../../../node_modules/.vite/packages/riviere-builder', + test: { + name: '@living-architecture/riviere-builder-domain-model', + watch: false, + globals: true, + environment: 'node', + include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + enabled: true, + reportsDirectory: './test-output/vitest/coverage', + provider: 'v8' as const, + reporter: ['text', ['lcov', { projectRoot: repoRoot }]] as [ + 'text', + ['lcov', { projectRoot: string }], + ], + thresholds: { + lines: 100, + statements: 100, + functions: 100, + branches: 100, + }, + }, + }, +})) diff --git a/packages/riviere-builder/package.json b/packages/riviere-builder/package.json deleted file mode 100644 index 5937b7b46..000000000 --- a/packages/riviere-builder/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "@living-architecture/riviere-builder", - "version": "0.10.3", - "publishConfig": { - "access": "public" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/NTCoding/living-architecture.git", - "directory": "packages/riviere-builder" - }, - "type": "module", - "main": "./dist/index.js", - "module": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - "./package.json": "./package.json", - ".": { - "@living-architecture/source": "./src/index.ts", - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "default": "./dist/index.js" - } - }, - "files": [ - "dist", - "!**/*.tsbuildinfo" - ], - "dependencies": { - "@living-architecture/riviere-query": "workspace:*", - "@living-architecture/riviere-schema": "workspace:*" - } -} diff --git a/packages/riviere-builder/project.json b/packages/riviere-builder/project.json deleted file mode 100644 index bf79ce85a..000000000 --- a/packages/riviere-builder/project.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "riviere-builder", - "targets": { - "typedoc": { - "executor": "nx:run-commands", - "options": { - "command": "pnpm exec typedoc --options packages/riviere-builder/typedoc.json", - "cwd": "{workspaceRoot}" - }, - "dependsOn": ["build"] - } - } -} diff --git a/packages/riviere-builder/src/features/building/domain/builder-facade.ts b/packages/riviere-builder/src/features/building/domain/builder-facade.ts deleted file mode 100644 index 1c5b5ff11..000000000 --- a/packages/riviere-builder/src/features/building/domain/builder-facade.ts +++ /dev/null @@ -1,387 +0,0 @@ -import type { - APIComponent, - CustomComponent, - DomainOpComponent, - EventComponent, - EventHandlerComponent, - ExternalLink, - Link, - RiviereGraph, - SourceInfo, - UIComponent, - UseCaseComponent, -} from '@living-architecture/riviere-schema' -import type { ValidationResult } from '@living-architecture/riviere-query' -import { RiviereBuilder as DomainBuilder } from './riviere-builder' -import type { - APIInput, - BuilderOptions, - CustomInput, - CustomTypeInput, - DomainInput, - DomainOpInput, - EventHandlerInput, - EventInput, - RelationshipTypeInput, - UpsertOptions, - UIInput, - UseCaseInput, -} from './construction/construction-types' -import type { EnrichmentInput } from './enrichment/enrichment-types' -import type { - NearMatchMismatch, - NearMatchOptions, - NearMatchQuery, - NearMatchResult, -} from './error-recovery/match-types' -import type { - BuilderStats, BuilderWarning -} from './inspection/inspection-types' -import type { - ExternalLinkInput, LinkInput -} from './linking/linking-types' - -export type { - APIInput, - BuilderOptions, - BuilderStats, - BuilderWarning, - CustomInput, - CustomTypeInput, - DomainInput, - DomainOpInput, - EnrichmentInput, - EventHandlerInput, - EventInput, - RelationshipTypeInput, - ExternalLinkInput, - LinkInput, - NearMatchMismatch, - NearMatchOptions, - NearMatchQuery, - NearMatchResult, - UpsertOptions, - UIInput, - UseCaseInput, -} - -/** - * Programmatically construct Riviere architecture graphs. - * - * Thin facade preserving the flat public API while delegating - * to focused domain classes internally. - * - * @riviere-role aggregate - */ -export class RiviereBuilder { - private readonly delegate: DomainBuilder - - readonly graphPath: string - - private constructor(delegate: DomainBuilder) { - this.delegate = delegate - this.graphPath = delegate.graphPath - } - - /** - * Restores a builder from a previously serialized graph. - * - * @param graph - A valid RiviereGraph to resume from - * @param graphPath - File path where the graph is persisted - * @returns A new RiviereBuilder with the graph state restored - */ - static resume(graph: RiviereGraph, graphPath = ''): RiviereBuilder { - return new RiviereBuilder(DomainBuilder.resume(graph, graphPath)) - } - - /** - * Creates a new builder with initial configuration. - * - * @param options - Configuration including sources and domains - * @param graphPath - File path where the graph will be persisted - * @returns A new RiviereBuilder instance - */ - static new(options: BuilderOptions, graphPath = ''): RiviereBuilder { - return new RiviereBuilder(DomainBuilder.new(options, graphPath)) - } - - /** - * Adds an additional source repository to the graph. - * - * @param source - Source repository information - */ - addSource(source: SourceInfo): void { - this.delegate.construction.addSource(source) - } - - /** - * Adds a new domain to the graph. - * - * @param input - Domain name and description - */ - addDomain(input: DomainInput): void { - this.delegate.construction.addDomain(input) - } - - /** - * Adds a UI component to the graph. - * - * @param input - UI component properties - * @returns The created UI component - */ - addUI(input: UIInput): UIComponent { - return this.delegate.construction.addUI(input) - } - - upsertUI( - input: UIInput, - options?: UpsertOptions, - ): { - component: UIComponent - created: boolean - } { - return this.delegate.construction.upsertUI(input, options) - } - - /** - * Adds an API component to the graph. - * - * @param input - API component properties - * @returns The created API component - */ - addApi(input: APIInput): APIComponent { - return this.delegate.construction.addApi(input) - } - - upsertApi( - input: APIInput, - options?: UpsertOptions, - ): { - component: APIComponent - created: boolean - } { - return this.delegate.construction.upsertApi(input, options) - } - - /** - * Adds a UseCase component to the graph. - * - * @param input - UseCase component properties - * @returns The created UseCase component - */ - addUseCase(input: UseCaseInput): UseCaseComponent { - return this.delegate.construction.addUseCase(input) - } - - upsertUseCase( - input: UseCaseInput, - options?: UpsertOptions, - ): { - component: UseCaseComponent - created: boolean - } { - return this.delegate.construction.upsertUseCase(input, options) - } - - /** - * Adds a DomainOp component to the graph. - * - * @param input - DomainOp component properties - * @returns The created DomainOp component - */ - addDomainOp(input: DomainOpInput): DomainOpComponent { - return this.delegate.construction.addDomainOp(input) - } - - upsertDomainOp( - input: DomainOpInput, - options?: UpsertOptions, - ): { - component: DomainOpComponent - created: boolean - } { - return this.delegate.construction.upsertDomainOp(input, options) - } - - /** - * Adds an Event component to the graph. - * - * @param input - Event component properties - * @returns The created Event component - */ - addEvent(input: EventInput): EventComponent { - return this.delegate.construction.addEvent(input) - } - - upsertEvent( - input: EventInput, - options?: UpsertOptions, - ): { - component: EventComponent - created: boolean - } { - return this.delegate.construction.upsertEvent(input, options) - } - - /** - * Adds an EventHandler component to the graph. - * - * @param input - EventHandler component properties - * @returns The created EventHandler component - */ - addEventHandler(input: EventHandlerInput): EventHandlerComponent { - return this.delegate.construction.addEventHandler(input) - } - - upsertEventHandler( - input: EventHandlerInput, - options?: UpsertOptions, - ): { - component: EventHandlerComponent - created: boolean - } { - return this.delegate.construction.upsertEventHandler(input, options) - } - - /** - * Defines a custom component type for the graph. - * - * @param input - Custom type definition - */ - defineCustomType(input: CustomTypeInput): void { - this.delegate.construction.defineCustomType(input) - } - - /** - * Defines a relationship type for the graph. - * - * @param input - Relationship type name and description - */ - defineRelationshipType(input: RelationshipTypeInput): void { - this.delegate.construction.defineRelationshipType(input) - } - - /** - * Adds a Custom component to the graph. - * - * @param input - Custom component properties - * @returns The created Custom component - */ - addCustom(input: CustomInput): CustomComponent { - return this.delegate.construction.addCustom(input) - } - - upsertCustom( - input: CustomInput, - options?: UpsertOptions, - ): { - component: CustomComponent - created: boolean - } { - return this.delegate.construction.upsertCustom(input, options) - } - - /** - * Enriches a DomainOp component with additional domain details. - * - * @param id - The component ID to enrich - * @param enrichment - State changes and business rules to add - */ - enrichComponent(id: string, enrichment: EnrichmentInput): void { - this.delegate.enrichment.enrichComponent(id, enrichment) - } - - /** - * Finds components similar to a query for error recovery. - * - * @param query - Search criteria including partial ID, name, type, or domain - * @param options - Optional matching thresholds and limits - * @returns Array of similar components with similarity scores - */ - nearMatches(query: NearMatchQuery, options?: NearMatchOptions): NearMatchResult[] { - return this.delegate.errorRecovery.findNearMatches(query, options) - } - - /** - * Creates a link between two components in the graph. - * - * @param input - Link properties including source, target, and type - * @returns The created link - */ - link(input: LinkInput): Link { - return this.delegate.linking.link(input) - } - - /** - * Creates a link from a component to an external system. - * - * @param input - External link properties including target system info - * @returns The created external link - */ - linkExternal(input: ExternalLinkInput): ExternalLink { - return this.delegate.linking.linkExternal(input) - } - - /** - * Returns non-fatal issues found in the graph. - * - * @returns Array of warning objects with type and message - */ - warnings(): BuilderWarning[] { - return this.delegate.inspection.warnings() - } - - /** - * Returns statistics about the current graph state. - * - * @returns Counts of components by type, domains, and links - */ - stats(): BuilderStats { - return this.delegate.inspection.stats() - } - - /** - * Runs full validation on the graph. - * - * @returns Validation result with valid flag and error details - */ - validate(): ValidationResult { - return this.delegate.inspection.validate() - } - - /** - * Returns IDs of components with no incoming or outgoing links. - * - * @returns Array of orphaned component IDs - */ - orphans(): string[] { - return this.delegate.inspection.orphans() - } - - /** - * Returns a RiviereQuery instance for the current graph state. - * - * @returns RiviereQuery instance for the current graph - */ - query(): import('@living-architecture/riviere-query').RiviereQuery { - return this.delegate.inspection.query() - } - - /** - * Serializes the current graph state as a JSON string. - * - * @returns JSON string representation of the graph - */ - serialize(): string { - return this.delegate.serialize() - } - - /** - * Validates and returns the completed graph. - * - * @returns Valid RiviereGraph object - */ - build(): RiviereGraph { - return this.delegate.build() - } -} diff --git a/packages/riviere-builder/src/features/building/domain/builder-graph.ts b/packages/riviere-builder/src/features/building/domain/builder-graph.ts deleted file mode 100644 index 163e874b5..000000000 --- a/packages/riviere-builder/src/features/building/domain/builder-graph.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { - CustomTypeDefinition, - ExternalLink, - GraphMetadata, - RiviereGraph, - SourceInfo, - RelationshipTypeDefinition, -} from '@living-architecture/riviere-schema' - -/** @riviere-role value-object */ -export interface BuilderMetadata extends Omit< - GraphMetadata, - 'sources' | 'customTypes' | 'relationshipTypes' -> { - sources: SourceInfo[] - customTypes: Record - relationshipTypes: Record -} - -/** @riviere-role value-object */ -export interface BuilderGraph extends Omit { - metadata: BuilderMetadata - externalLinks: ExternalLink[] -} diff --git a/packages/riviere-builder/src/features/building/domain/builder-query.spec.ts b/packages/riviere-builder/src/features/building/domain/builder-query.spec.ts deleted file mode 100644 index e1a51c0e7..000000000 --- a/packages/riviere-builder/src/features/building/domain/builder-query.spec.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { RiviereQuery } from '@living-architecture/riviere-query' -import { - RiviereBuilder, type BuilderOptions -} from './builder-facade' - -function createValidOptions(): BuilderOptions { - return { - sources: [ - { - repository: 'my-org/my-repo', - commit: 'abc123', - }, - ], - domains: { - orders: { - description: 'Order management', - systemType: 'domain', - }, - }, - } -} - -describe('query', () => { - it('returns RiviereQuery instance when builder has components', () => { - const builder = RiviereBuilder.new(createValidOptions()) - builder.addApi({ - name: 'Create Order', - domain: 'orders', - module: 'checkout', - apiType: 'REST', - httpMethod: 'POST', - path: '/orders', - sourceLocation: { - repository: 'my-org/my-repo', - filePath: 'src/orders.ts', - }, - }) - - const result = builder.query() - - expect(result).toBeInstanceOf(RiviereQuery) - }) - - it('returns APIs via componentsByType when builder has API components', () => { - const builder = RiviereBuilder.new(createValidOptions()) - builder.addApi({ - name: 'Create Order', - domain: 'orders', - module: 'checkout', - apiType: 'REST', - httpMethod: 'POST', - path: '/orders', - sourceLocation: { - repository: 'my-org/my-repo', - filePath: 'src/orders.ts', - }, - }) - - const apis = builder.query().componentsByType('API') - - expect(apis).toHaveLength(1) - expect(apis[0]?.name).toBe('Create Order') - }) - - it('returns query instance without throwing when builder has orphan components', () => { - const builder = RiviereBuilder.new(createValidOptions()) - builder.addApi({ - name: 'Orphan API', - domain: 'orders', - module: 'checkout', - apiType: 'REST', - httpMethod: 'GET', - path: '/orphan', - sourceLocation: { - repository: 'my-org/my-repo', - filePath: 'src/orphan.ts', - }, - }) - - expect(() => builder.query()).not.toThrow() - }) - - it('includes newly added components in subsequent query calls', () => { - const builder = RiviereBuilder.new(createValidOptions()) - - const beforeApis = builder.query().componentsByType('API') - expect(beforeApis).toHaveLength(0) - - builder.addApi({ - name: 'New API', - domain: 'orders', - module: 'checkout', - apiType: 'REST', - httpMethod: 'POST', - path: '/new', - sourceLocation: { - repository: 'my-org/my-repo', - filePath: 'src/new.ts', - }, - }) - - const afterApis = builder.query().componentsByType('API') - expect(afterApis).toHaveLength(1) - expect(afterApis[0]?.name).toBe('New API') - }) - - it('returns updated component count after each add operation', () => { - const builder = RiviereBuilder.new(createValidOptions()) - - expect(builder.query().componentsByType('API')).toHaveLength(0) - - builder.addApi({ - name: 'First API', - domain: 'orders', - module: 'checkout', - apiType: 'REST', - httpMethod: 'GET', - path: '/first', - sourceLocation: { - repository: 'my-org/my-repo', - filePath: 'src/first.ts', - }, - }) - - expect(builder.query().componentsByType('API')).toHaveLength(1) - - builder.addApi({ - name: 'Second API', - domain: 'orders', - module: 'checkout', - apiType: 'REST', - httpMethod: 'GET', - path: '/second', - sourceLocation: { - repository: 'my-org/my-repo', - filePath: 'src/second.ts', - }, - }) - - expect(builder.query().componentsByType('API')).toHaveLength(2) - }) -}) diff --git a/packages/riviere-builder/src/features/building/domain/construction/builder-internals.ts b/packages/riviere-builder/src/features/building/domain/construction/builder-internals.ts deleted file mode 100644 index d45a9ec97..000000000 --- a/packages/riviere-builder/src/features/building/domain/construction/builder-internals.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { - Component, - CustomTypeDefinition, - DomainMetadata, -} from '@living-architecture/riviere-schema' -import { ComponentId } from '@living-architecture/riviere-schema' -import { createSourceNotFoundError } from '../error-recovery/component-suggestion' -import { - assertCustomTypeExists, - assertDomainExists, - assertRequiredPropertiesProvided, -} from './builder-assertions' - -/** @riviere-role domain-service */ -export function generateComponentId( - domain: string, - module: string, - type: string, - name: string, -): string { - const nameSegment = name.toLowerCase().replaceAll(/\s+/g, '-') - return `${domain}:${module}:${type}:${nameSegment}` -} - -/** @riviere-role domain-service */ -export function createComponentNotFoundError(components: Component[], id: string): Error { - return createSourceNotFoundError(components, ComponentId.parse(id)) -} - -/** @riviere-role domain-service */ -export function validateDomainExists( - domains: Record, - domain: string, -): void { - assertDomainExists(domains, domain) -} - -/** @riviere-role domain-service */ -export function validateCustomType( - customTypes: Record, - customTypeName: string, -): void { - assertCustomTypeExists(customTypes, customTypeName) -} - -/** @riviere-role domain-service */ -export function validateRequiredProperties( - customTypes: Record, - customTypeName: string, - metadata: Record | undefined, -): void { - assertRequiredPropertiesProvided(customTypes, customTypeName, metadata) -} diff --git a/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts b/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts deleted file mode 100644 index 3d2a92ec7..000000000 --- a/packages/riviere-builder/src/features/building/domain/construction/construction-types.ts +++ /dev/null @@ -1,129 +0,0 @@ -import type { - ApiType, - CustomPropertyDefinition, - DomainMetadata, - HttpMethod, - OperationBehavior, - OperationSignature, - SourceInfo, - SourceLocation, - StateTransition, - SystemType, -} from '@living-architecture/riviere-schema' - -/** @riviere-role value-object */ -export interface BuilderOptions { - name?: string - description?: string - sources: SourceInfo[] - domains: Record -} - -/** @riviere-role value-object */ -export interface DomainInput { - name: string - description: string - systemType: SystemType -} - -/** @riviere-role value-object */ -export interface UpsertOptions {noOverwrite?: boolean} - -/** @riviere-role value-object */ -export interface UIInput { - name: string - domain: string - module: string - route: string - description?: string - sourceLocation: SourceLocation - metadata?: Record -} - -/** @riviere-role value-object */ -export interface APIInput { - name: string - domain: string - module: string - apiType: ApiType - httpMethod?: HttpMethod - path?: string - operationName?: string - description?: string - sourceLocation: SourceLocation - metadata?: Record -} - -/** @riviere-role value-object */ -export interface UseCaseInput { - name: string - domain: string - module: string - description?: string - sourceLocation: SourceLocation - metadata?: Record -} - -/** @riviere-role value-object */ -export interface DomainOpInput { - name: string - domain: string - module: string - operationName: string - entity?: string - signature?: OperationSignature - behavior?: OperationBehavior - stateChanges?: StateTransition[] - businessRules?: string[] - description?: string - sourceLocation: SourceLocation - metadata?: Record -} - -/** @riviere-role value-object */ -export interface EventInput { - name: string - domain: string - module: string - eventName: string - eventSchema?: string - description?: string - sourceLocation: SourceLocation - metadata?: Record -} - -/** @riviere-role value-object */ -export interface EventHandlerInput { - name: string - domain: string - module: string - subscribedEvents: string[] - description?: string - sourceLocation: SourceLocation - metadata?: Record -} - -/** @riviere-role value-object */ -export interface CustomTypeInput { - name: string - description?: string - requiredProperties?: Record - optionalProperties?: Record -} - -/** @riviere-role value-object */ -export interface RelationshipTypeInput { - name: string - description: string -} - -/** @riviere-role value-object */ -export interface CustomInput { - customTypeName: string - name: string - domain: string - module: string - description?: string - sourceLocation: SourceLocation - metadata?: Record -} diff --git a/packages/riviere-builder/src/features/building/domain/construction/errors.spec.ts b/packages/riviere-builder/src/features/building/domain/construction/errors.spec.ts deleted file mode 100644 index 96b1460b2..000000000 --- a/packages/riviere-builder/src/features/building/domain/construction/errors.spec.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { - describe, it, expect -} from 'vitest' -import { - ComponentNotFoundError, - CustomTypeNotFoundError, - DomainNotFoundError, - DuplicateComponentError, - DuplicateDomainError, - SourceConflictError, - ComponentTypeMismatchError, - CustomTypeAlreadyDefinedError, - MissingRequiredPropertiesError, - InvalidGraphError, - MissingSourcesError, - MissingDomainsError, - BuildValidationError, - DuplicateLinkError, - RelationshipTypeAlreadyDefinedError, - RelationshipTypeNotFoundError, -} from './construction-errors' -import { InvalidEnrichmentTargetError } from '../enrichment/enrichment-errors' - -describe('errors', () => { - describe('DuplicateDomainError', () => { - it('includes domain name in message', () => { - const error = new DuplicateDomainError('orders') - - expect(error.message).toBe("Domain 'orders' already exists") - expect(error.domainName).toBe('orders') - expect(error.name).toBe('DuplicateDomainError') - }) - }) - - describe('SourceConflictError', () => { - it('includes repository in message', () => { - const error = new SourceConflictError('test/repo') - - expect(error.message).toBe("Source 'test/repo' already exists with different values") - expect(error.repository).toBe('test/repo') - expect(error.name).toBe('SourceConflictError') - }) - }) - - describe('DomainNotFoundError', () => { - it('includes domain name in message', () => { - const error = new DomainNotFoundError('orders') - - expect(error.message).toBe("Domain 'orders' does not exist") - expect(error.domainName).toBe('orders') - expect(error.name).toBe('DomainNotFoundError') - }) - }) - - describe('CustomTypeNotFoundError', () => { - it('includes custom type name and defined types in message', () => { - const error = new CustomTypeNotFoundError('Queue', ['Worker', 'Cache']) - - expect(error.message).toBe("Custom type 'Queue' not defined. Defined types: Worker, Cache") - expect(error.customTypeName).toBe('Queue') - expect(error.definedTypes).toStrictEqual(['Worker', 'Cache']) - expect(error.name).toBe('CustomTypeNotFoundError') - }) - - it('handles empty defined types', () => { - const error = new CustomTypeNotFoundError('Queue', []) - - expect(error.message).toBe( - "Custom type 'Queue' not defined. No custom types have been defined.", - ) - }) - }) - - describe('DuplicateComponentError', () => { - it('includes component ID in message', () => { - const error = new DuplicateComponentError('orders:checkout:api:create-order') - - expect(error.message).toBe( - "Component with ID 'orders:checkout:api:create-order' already exists", - ) - expect(error.componentId).toBe('orders:checkout:api:create-order') - expect(error.name).toBe('DuplicateComponentError') - }) - }) - - describe('ComponentTypeMismatchError', () => { - it('includes component identity and types in message', () => { - const error = new ComponentTypeMismatchError('orders:checkout:ui:checkout-page', 'UI', 'API') - - expect(error.message).toBe( - "Component 'orders:checkout:ui:checkout-page' already exists as type 'UI'; cannot upsert as 'API'", - ) - expect(error.componentId).toBe('orders:checkout:ui:checkout-page') - expect(error.existingType).toBe('UI') - expect(error.incomingType).toBe('API') - }) - }) - - describe('ComponentNotFoundError', () => { - it('includes component ID and empty suggestions by default', () => { - const error = new ComponentNotFoundError('orders:checkout:api:create-ordr') - - expect(error.message).toBe("Source component 'orders:checkout:api:create-ordr' not found") - expect(error.componentId).toBe('orders:checkout:api:create-ordr') - expect(error.suggestions).toStrictEqual([]) - expect(error.name).toBe('ComponentNotFoundError') - }) - - it('includes suggestions in message when provided', () => { - const error = new ComponentNotFoundError('orders:checkout:api:create-ordr', [ - 'orders:checkout:api:create-order', - 'orders:checkout:api:update-order', - ]) - - expect(error.message).toBe( - "Source component 'orders:checkout:api:create-ordr' not found. Did you mean: orders:checkout:api:create-order, orders:checkout:api:update-order?", - ) - expect(error.suggestions).toStrictEqual([ - 'orders:checkout:api:create-order', - 'orders:checkout:api:update-order', - ]) - }) - }) - - describe('InvalidEnrichmentTargetError', () => { - it('includes component ID and type in message', () => { - const error = new InvalidEnrichmentTargetError('orders:api:create', 'API') - - expect(error.message).toBe( - "Only DomainOp components can be enriched. 'orders:api:create' is type 'API'", - ) - expect(error.componentId).toBe('orders:api:create') - expect(error.componentType).toBe('API') - expect(error.name).toBe('InvalidEnrichmentTargetError') - }) - }) - - describe('CustomTypeAlreadyDefinedError', () => { - it('includes type name in message', () => { - const error = new CustomTypeAlreadyDefinedError('Worker') - - expect(error.message).toBe("Custom type 'Worker' already defined") - expect(error.typeName).toBe('Worker') - expect(error.name).toBe('CustomTypeAlreadyDefinedError') - }) - }) - - describe('RelationshipTypeAlreadyDefinedError', () => { - it('includes the relationship type name in the message', () => { - const error = new RelationshipTypeAlreadyDefinedError('reads') - - expect(error.message).toBe("Relationship type 'reads' already defined") - expect(error.typeName).toBe('reads') - expect(error.name).toBe('RelationshipTypeAlreadyDefinedError') - }) - }) - - describe('RelationshipTypeNotFoundError', () => { - it('includes the relationship type and available types in the message', () => { - const error = new RelationshipTypeNotFoundError('queries', ['reads', 'writes']) - - expect(error.message).toBe( - "Relationship type 'queries' not defined. Defined types: reads, writes", - ) - expect(error.relationshipType).toBe('queries') - expect(error.definedTypes).toStrictEqual(['reads', 'writes']) - expect(error.name).toBe('RelationshipTypeNotFoundError') - }) - }) - - describe('DuplicateLinkError', () => { - it('includes the Link ID in the message', () => { - const error = new DuplicateLinkError('source->target@file.sql:12:5') - - expect(error.message).toBe("Link with ID 'source->target@file.sql:12:5' already exists") - expect(error.linkId).toBe('source->target@file.sql:12:5') - expect(error.name).toBe('DuplicateLinkError') - }) - }) - - describe('MissingRequiredPropertiesError', () => { - it('includes custom type name and missing keys in message', () => { - const error = new MissingRequiredPropertiesError('Worker', ['queueName', 'concurrency']) - - expect(error.message).toBe("Missing required properties for 'Worker': queueName, concurrency") - expect(error.customTypeName).toBe('Worker') - expect(error.missingKeys).toStrictEqual(['queueName', 'concurrency']) - expect(error.name).toBe('MissingRequiredPropertiesError') - }) - }) - - describe('InvalidGraphError', () => { - it('includes reason in message', () => { - const error = new InvalidGraphError('missing version') - - expect(error.message).toBe('Invalid graph: missing version') - expect(error.name).toBe('InvalidGraphError') - }) - }) - - describe('MissingSourcesError', () => { - it('sets message', () => { - const error = new MissingSourcesError() - - expect(error.message).toBe('At least one source required') - expect(error.name).toBe('MissingSourcesError') - }) - }) - - describe('MissingDomainsError', () => { - it('sets message', () => { - const error = new MissingDomainsError() - - expect(error.message).toBe('At least one domain required') - expect(error.name).toBe('MissingDomainsError') - }) - }) - - describe('BuildValidationError', () => { - it('includes validation messages in message', () => { - const error = new BuildValidationError(['error 1', 'error 2']) - - expect(error.message).toBe('Validation failed: error 1; error 2') - expect(error.validationMessages).toStrictEqual(['error 1', 'error 2']) - expect(error.name).toBe('BuildValidationError') - }) - }) -}) diff --git a/packages/riviere-builder/src/features/building/domain/construction/graph-construction.ts b/packages/riviere-builder/src/features/building/domain/construction/graph-construction.ts deleted file mode 100644 index 106ffca32..000000000 --- a/packages/riviere-builder/src/features/building/domain/construction/graph-construction.ts +++ /dev/null @@ -1,395 +0,0 @@ -import type { - APIComponent, - Component, - CustomComponent, - DomainOpComponent, - EventComponent, - EventHandlerComponent, - SourceInfo, - UIComponent, - UseCaseComponent, -} from '@living-architecture/riviere-schema' -import type { BuilderGraph } from '../builder-graph' -import type { - APIInput, - CustomInput, - CustomTypeInput, - DomainInput, - DomainOpInput, - EventHandlerInput, - EventInput, - RelationshipTypeInput, - UpsertOptions, - UIInput, - UseCaseInput, -} from './construction-types' -import { - ComponentTypeMismatchError, - CustomTypeAlreadyDefinedError, - DuplicateComponentError, - DuplicateDomainError, - SourceConflictError, - RelationshipTypeAlreadyDefinedError, -} from './construction-errors' -import { - generateComponentId, - validateCustomType, - validateDomainExists, - validateRequiredProperties, -} from './builder-internals' -import { mergeComponentForUpsert } from '../enrichment/upsert-merge' -import type { BuilderWarning } from '../inspection/inspection-types' - -/** @riviere-role domain-service */ -export class GraphConstruction { - private readonly graph: BuilderGraph - private readonly operationWarnings: BuilderWarning[] - - constructor(graph: BuilderGraph, operationWarnings: BuilderWarning[]) { - this.graph = graph - this.operationWarnings = operationWarnings - } - - addSource(source: SourceInfo): void { - const existing = this.graph.metadata.sources.find( - (item) => item.repository === source.repository, - ) - if (existing) { - if (areSourcesEqual(existing, source)) { - return - } - - throw new SourceConflictError(source.repository) - } - - this.graph.metadata.sources.push(source) - } - - addDomain(input: DomainInput): void { - const existing = this.graph.metadata.domains[input.name] - if (existing) { - if (existing.description === input.description && existing.systemType === input.systemType) { - return - } - - throw new DuplicateDomainError(input.name) - } - - this.graph.metadata.domains[input.name] = { - description: input.description, - systemType: input.systemType, - } - } - - addUI(input: UIInput): UIComponent { - return this.registerComponent(this.buildUIComponent(input)) - } - - upsertUI( - input: UIInput, - options?: UpsertOptions, - ): { - component: UIComponent - created: boolean - } { - return this.upsertTypedComponent(this.buildUIComponent(input), options) - } - - addApi(input: APIInput): APIComponent { - return this.registerComponent(this.buildAPIComponent(input)) - } - - upsertApi( - input: APIInput, - options?: UpsertOptions, - ): { - component: APIComponent - created: boolean - } { - return this.upsertTypedComponent(this.buildAPIComponent(input), options) - } - - addUseCase(input: UseCaseInput): UseCaseComponent { - return this.registerComponent(this.buildUseCaseComponent(input)) - } - - upsertUseCase( - input: UseCaseInput, - options?: UpsertOptions, - ): { - component: UseCaseComponent - created: boolean - } { - return this.upsertTypedComponent(this.buildUseCaseComponent(input), options) - } - - addDomainOp(input: DomainOpInput): DomainOpComponent { - return this.registerComponent(this.buildDomainOpComponent(input)) - } - - upsertDomainOp( - input: DomainOpInput, - options?: UpsertOptions, - ): { - component: DomainOpComponent - created: boolean - } { - return this.upsertTypedComponent(this.buildDomainOpComponent(input), options) - } - - addEvent(input: EventInput): EventComponent { - return this.registerComponent(this.buildEventComponent(input)) - } - - upsertEvent( - input: EventInput, - options?: UpsertOptions, - ): { - component: EventComponent - created: boolean - } { - return this.upsertTypedComponent(this.buildEventComponent(input), options) - } - - addEventHandler(input: EventHandlerInput): EventHandlerComponent { - return this.registerComponent(this.buildEventHandlerComponent(input)) - } - - upsertEventHandler( - input: EventHandlerInput, - options?: UpsertOptions, - ): { - component: EventHandlerComponent - created: boolean - } { - return this.upsertTypedComponent(this.buildEventHandlerComponent(input), options) - } - - defineCustomType(input: CustomTypeInput): void { - const customTypes = this.graph.metadata.customTypes - - if (customTypes[input.name]) { - throw new CustomTypeAlreadyDefinedError(input.name) - } - - customTypes[input.name] = { - ...(input.requiredProperties !== undefined && {requiredProperties: input.requiredProperties,}), - ...(input.optionalProperties !== undefined && {optionalProperties: input.optionalProperties,}), - ...(input.description !== undefined && { description: input.description }), - } - } - - defineRelationshipType(input: RelationshipTypeInput): void { - const relationshipTypes = this.graph.metadata.relationshipTypes - if (Object.hasOwn(relationshipTypes, input.name)) { - throw new RelationshipTypeAlreadyDefinedError(input.name) - } - - Object.defineProperty(relationshipTypes, input.name, { - value: { description: input.description }, - enumerable: true, - configurable: true, - writable: true, - }) - } - - addCustom(input: CustomInput): CustomComponent { - return this.registerComponent(this.buildCustomComponent(input)) - } - - upsertCustom( - input: CustomInput, - options?: UpsertOptions, - ): { - component: CustomComponent - created: boolean - } { - return this.upsertTypedComponent(this.buildCustomComponent(input), options) - } - - private buildUIComponent(input: UIInput): UIComponent { - validateDomainExists(this.graph.metadata.domains, input.domain) - const id = generateComponentId(input.domain, input.module, 'ui', input.name) - - return { - id, - type: 'UI', - name: input.name, - domain: input.domain, - module: input.module, - route: input.route, - sourceLocation: input.sourceLocation, - ...(input.description !== undefined && { description: input.description }), - } - } - - private buildAPIComponent(input: APIInput): APIComponent { - validateDomainExists(this.graph.metadata.domains, input.domain) - const id = generateComponentId(input.domain, input.module, 'api', input.name) - - return { - id, - type: 'API', - name: input.name, - domain: input.domain, - module: input.module, - apiType: input.apiType, - sourceLocation: input.sourceLocation, - ...(input.httpMethod !== undefined && { httpMethod: input.httpMethod }), - ...(input.path !== undefined && { path: input.path }), - ...(input.operationName !== undefined && { operationName: input.operationName }), - ...(input.description !== undefined && { description: input.description }), - } - } - - private buildUseCaseComponent(input: UseCaseInput): UseCaseComponent { - validateDomainExists(this.graph.metadata.domains, input.domain) - const id = generateComponentId(input.domain, input.module, 'usecase', input.name) - - return { - id, - type: 'UseCase', - name: input.name, - domain: input.domain, - module: input.module, - sourceLocation: input.sourceLocation, - ...(input.description !== undefined && { description: input.description }), - } - } - - private buildDomainOpComponent(input: DomainOpInput): DomainOpComponent { - validateDomainExists(this.graph.metadata.domains, input.domain) - const id = generateComponentId(input.domain, input.module, 'domainop', input.name) - - return { - id, - type: 'DomainOp', - name: input.name, - domain: input.domain, - module: input.module, - operationName: input.operationName, - sourceLocation: input.sourceLocation, - ...(input.entity !== undefined && { entity: input.entity }), - ...(input.signature !== undefined && { signature: input.signature }), - ...(input.behavior !== undefined && { behavior: input.behavior }), - ...(input.stateChanges !== undefined && { stateChanges: input.stateChanges }), - ...(input.businessRules !== undefined && { businessRules: input.businessRules }), - ...(input.description !== undefined && { description: input.description }), - } - } - - private buildEventComponent(input: EventInput): EventComponent { - validateDomainExists(this.graph.metadata.domains, input.domain) - const id = generateComponentId(input.domain, input.module, 'event', input.name) - - return { - id, - type: 'Event', - name: input.name, - domain: input.domain, - module: input.module, - eventName: input.eventName, - sourceLocation: input.sourceLocation, - ...(input.eventSchema !== undefined && { eventSchema: input.eventSchema }), - ...(input.description !== undefined && { description: input.description }), - } - } - - private buildEventHandlerComponent(input: EventHandlerInput): EventHandlerComponent { - validateDomainExists(this.graph.metadata.domains, input.domain) - const id = generateComponentId(input.domain, input.module, 'eventhandler', input.name) - - return { - id, - type: 'EventHandler', - name: input.name, - domain: input.domain, - module: input.module, - subscribedEvents: input.subscribedEvents, - sourceLocation: input.sourceLocation, - ...(input.description !== undefined && { description: input.description }), - } - } - - private buildCustomComponent(input: CustomInput): CustomComponent { - validateDomainExists(this.graph.metadata.domains, input.domain) - validateCustomType(this.graph.metadata.customTypes, input.customTypeName) - validateRequiredProperties( - this.graph.metadata.customTypes, - input.customTypeName, - input.metadata, - ) - const id = generateComponentId(input.domain, input.module, 'custom', input.name) - - const component: CustomComponent = { - id, - type: 'Custom', - customTypeName: input.customTypeName, - name: input.name, - domain: input.domain, - module: input.module, - sourceLocation: input.sourceLocation, - ...(input.description !== undefined && { description: input.description }), - ...input.metadata, - } - - return component - } - - private registerComponent(component: T): T { - if (this.graph.components.some((c) => c.id === component.id)) { - throw new DuplicateComponentError(component.id) - } - this.graph.components.push(component) - return component - } - - private upsertTypedComponent( - incoming: T, - options?: UpsertOptions, - ): { - component: T - created: boolean - } { - const existingIndex = this.graph.components.findIndex( - (component) => component.id === incoming.id, - ) - if (existingIndex === -1) { - this.graph.components.push(incoming) - return { - component: incoming, - created: true, - } - } - - const existing = this.graph.components[existingIndex] - - if (!isSameTypeComponent(existing, incoming)) { - throw new ComponentTypeMismatchError(incoming.id, existing?.type ?? 'unknown', incoming.type) - } - - const merged = mergeComponentForUpsert(existing, incoming, options, this.operationWarnings) - - this.graph.components[existingIndex] = merged - - return { - component: merged, - created: false, - } - } -} - -function areSourcesEqual(existing: SourceInfo, incoming: SourceInfo): boolean { - return ( - existing.repository === incoming.repository && - existing.commit === incoming.commit && - existing.extractedAt === incoming.extractedAt - ) -} - -function isSameTypeComponent( - existing: Component | undefined, - incoming: T, -): existing is T { - return existing?.type === incoming.type -} diff --git a/packages/riviere-builder/src/features/building/domain/enrichment/enrichment-types.ts b/packages/riviere-builder/src/features/building/domain/enrichment/enrichment-types.ts deleted file mode 100644 index 2db827275..000000000 --- a/packages/riviere-builder/src/features/building/domain/enrichment/enrichment-types.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { - OperationBehavior, - OperationSignature, - StateTransition, -} from '@living-architecture/riviere-schema' - -/** @riviere-role value-object */ -export interface EnrichmentInput { - entity?: string - stateChanges?: StateTransition[] - businessRules?: string[] - behavior?: OperationBehavior - signature?: OperationSignature -} diff --git a/packages/riviere-builder/src/features/building/domain/enrichment/graph-enrichment.ts b/packages/riviere-builder/src/features/building/domain/enrichment/graph-enrichment.ts deleted file mode 100644 index ebadbc983..000000000 --- a/packages/riviere-builder/src/features/building/domain/enrichment/graph-enrichment.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { BuilderGraph } from '../builder-graph' -import type { EnrichmentInput } from './enrichment-types' -import { InvalidEnrichmentTargetError } from './enrichment-errors' -import { createComponentNotFoundError } from '../construction/builder-internals' -import { deduplicateStateTransitions } from './deduplicate-transitions' -import { deduplicateStrings } from '../../../../platform/domain/collection-utils/deduplicate-strings' -import { mergeBehavior } from './merge-behavior' - -/** @riviere-role domain-service */ -export class GraphEnrichment { - private readonly graph: BuilderGraph - - constructor(graph: BuilderGraph) { - this.graph = graph - } - - enrichComponent(id: string, enrichment: EnrichmentInput): void { - const component = this.graph.components.find((c) => c.id === id) - if (!component) { - throw createComponentNotFoundError(this.graph.components, id) - } - if (component.type !== 'DomainOp') { - throw new InvalidEnrichmentTargetError(id, component.type) - } - if (enrichment.entity !== undefined) { - component.entity = enrichment.entity - } - if (enrichment.stateChanges !== undefined) { - const existing = component.stateChanges ?? [] - const newItems = deduplicateStateTransitions(existing, enrichment.stateChanges) - component.stateChanges = [...existing, ...newItems] - } - if (enrichment.businessRules !== undefined) { - const existing = component.businessRules ?? [] - const newItems = deduplicateStrings(existing, enrichment.businessRules) - component.businessRules = [...existing, ...newItems] - } - if (enrichment.behavior !== undefined) { - component.behavior = mergeBehavior(component.behavior, enrichment.behavior) - } - if (enrichment.signature !== undefined) { - component.signature = enrichment.signature - } - } -} diff --git a/packages/riviere-builder/src/features/building/domain/enrichment/merge-behavior.ts b/packages/riviere-builder/src/features/building/domain/enrichment/merge-behavior.ts deleted file mode 100644 index 21d883c40..000000000 --- a/packages/riviere-builder/src/features/building/domain/enrichment/merge-behavior.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { - DomainOpComponent, OperationBehavior -} from '@living-architecture/riviere-schema' -import { deduplicateStrings } from '../../../../platform/domain/collection-utils/deduplicate-strings' - -function mergeStringArray(existing: string[] | undefined, incoming: string[]): string[] { - const base = existing ?? [] - return [...base, ...deduplicateStrings(base, incoming)] -} - -/** @riviere-role domain-service */ -export function mergeBehavior( - existing: DomainOpComponent['behavior'], - incoming: OperationBehavior, -): OperationBehavior { - const base = existing ?? {} - return { - ...base, - ...(incoming.reads !== undefined && { reads: mergeStringArray(base.reads, incoming.reads) }), - ...(incoming.validates !== undefined && {validates: mergeStringArray(base.validates, incoming.validates),}), - ...(incoming.modifies !== undefined && {modifies: mergeStringArray(base.modifies, incoming.modifies),}), - ...(incoming.emits !== undefined && { emits: mergeStringArray(base.emits, incoming.emits) }), - } -} diff --git a/packages/riviere-builder/src/features/building/domain/error-recovery/match-types.ts b/packages/riviere-builder/src/features/building/domain/error-recovery/match-types.ts deleted file mode 100644 index c4e316e6e..000000000 --- a/packages/riviere-builder/src/features/building/domain/error-recovery/match-types.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { ComponentType } from '@living-architecture/riviere-schema' - -/** @riviere-role value-object */ -export interface NearMatchQuery { - name: string - type?: ComponentType - domain?: string -} - -/** @riviere-role value-object */ -export interface NearMatchMismatch { - field: 'type' | 'domain' - expected: string - actual: string -} - -/** @riviere-role value-object */ -export interface NearMatchResult { - component: import('@living-architecture/riviere-schema').Component - score: number - mismatch?: NearMatchMismatch | undefined -} - -/** @riviere-role value-object */ -export interface NearMatchOptions { - threshold?: number - limit?: number -} diff --git a/packages/riviere-builder/src/features/building/domain/error-recovery/near-match.ts b/packages/riviere-builder/src/features/building/domain/error-recovery/near-match.ts deleted file mode 100644 index 95402e155..000000000 --- a/packages/riviere-builder/src/features/building/domain/error-recovery/near-match.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { BuilderGraph } from '../builder-graph' -import type { - NearMatchOptions, NearMatchQuery, NearMatchResult -} from './match-types' -import { findNearMatches } from './component-suggestion' - -/** @riviere-role domain-service */ -export class NearMatch { - private readonly graph: BuilderGraph - - constructor(graph: BuilderGraph) { - this.graph = graph - } - - findNearMatches(query: NearMatchQuery, options?: NearMatchOptions): NearMatchResult[] { - return findNearMatches(this.graph.components, query, options) - } -} diff --git a/packages/riviere-builder/src/features/building/domain/index.ts b/packages/riviere-builder/src/features/building/domain/index.ts deleted file mode 100644 index 6f54cd729..000000000 --- a/packages/riviere-builder/src/features/building/domain/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -export * from './builder-facade' -export { - ComponentId, type ComponentIdParts -} from '@living-architecture/riviere-schema' -export { - DuplicateDomainError, - SourceConflictError, - DomainNotFoundError, - CustomTypeNotFoundError, - DuplicateComponentError, - ComponentTypeMismatchError, - ComponentNotFoundError, - CustomTypeAlreadyDefinedError, - RelationshipTypeAlreadyDefinedError, - RelationshipTypeNotFoundError, - DuplicateLinkError, - MissingRequiredPropertiesError, - InvalidGraphError, - MissingSourcesError, - MissingDomainsError, - BuildValidationError, -} from './construction/construction-errors' -export { InvalidEnrichmentTargetError } from './enrichment/enrichment-errors' -export { findNearMatches } from './error-recovery/component-suggestion' diff --git a/packages/riviere-builder/src/features/building/domain/inspection/graph-inspection.ts b/packages/riviere-builder/src/features/building/domain/inspection/graph-inspection.ts deleted file mode 100644 index bb31f3d4e..000000000 --- a/packages/riviere-builder/src/features/building/domain/inspection/graph-inspection.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { ValidationResult } from '@living-architecture/riviere-query' -import { RiviereQuery } from '@living-architecture/riviere-query' -import type { BuilderGraph } from '../builder-graph' -import type { - BuilderStats, BuilderWarning -} from './inspection-types' -import { - calculateStats, - findOrphans, - findWarnings, - toRiviereGraph, - validateGraph, -} from './inspection-functions' - -/** @riviere-role domain-service */ -export class GraphInspection { - private readonly graph: BuilderGraph - private readonly operationWarnings: readonly BuilderWarning[] - - constructor(graph: BuilderGraph, operationWarnings: readonly BuilderWarning[]) { - this.graph = graph - this.operationWarnings = operationWarnings - } - - warnings(): BuilderWarning[] { - return [...findWarnings(this.graph), ...this.operationWarnings] - } - - stats(): BuilderStats { - return calculateStats(this.graph) - } - - orphans(): string[] { - return findOrphans(this.graph) - } - - validate(): ValidationResult { - return validateGraph(this.graph) - } - - query(): RiviereQuery { - return new RiviereQuery(toRiviereGraph(this.graph)) - } -} diff --git a/packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts b/packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts deleted file mode 100644 index e52a31907..000000000 --- a/packages/riviere-builder/src/features/building/domain/inspection/inspection-types.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** @riviere-role value-object */ -export interface BuilderStats { - componentCount: number - componentsByType: { - UI: number - API: number - UseCase: number - DomainOp: number - Event: number - EventHandler: number - Custom: number - } - linkCount: number - externalLinkCount: number - domainCount: number -} - -/** @riviere-role value-object */ -export type WarningCode = - | 'ORPHAN_COMPONENT' - | 'UNUSED_DOMAIN' - | 'SCALAR_OVERWRITE' - | 'DUPLICATE_LINK_SKIPPED' - -/** @riviere-role value-object */ -export interface BuilderWarning { - code: WarningCode - message: string - componentId?: string - domainName?: string - field?: string - oldValue?: string | number | boolean - newValue?: string | number | boolean - source?: string - target?: string - linkType?: string - targetRepository?: string - targetName?: string -} diff --git a/packages/riviere-builder/src/features/building/domain/linking/graph-linking.ts b/packages/riviere-builder/src/features/building/domain/linking/graph-linking.ts deleted file mode 100644 index 8af107cb5..000000000 --- a/packages/riviere-builder/src/features/building/domain/linking/graph-linking.ts +++ /dev/null @@ -1,102 +0,0 @@ -import type { - ExternalLink, Link -} from '@living-architecture/riviere-schema' -import { createLinkId } from '@living-architecture/riviere-schema' -import type { BuilderGraph } from '../builder-graph' -import type { BuilderWarning } from '../inspection/inspection-types' -import type { - ExternalLinkInput, LinkInput -} from './linking-types' -import { createComponentNotFoundError } from '../construction/builder-internals' -import { - DuplicateLinkError, - RelationshipTypeNotFoundError, -} from '../construction/construction-errors' - -/** @riviere-role domain-service */ -export class GraphLinking { - private readonly graph: BuilderGraph - private readonly operationWarnings: BuilderWarning[] - - constructor(graph: BuilderGraph, operationWarnings: BuilderWarning[]) { - this.graph = graph - this.operationWarnings = operationWarnings - } - - link(input: LinkInput): Link { - const sourceExists = this.graph.components.some((c) => c.id === input.from) - if (!sourceExists) { - throw createComponentNotFoundError(this.graph.components, input.from) - } - - if ( - input.relationshipType !== undefined && - !Object.hasOwn(this.graph.metadata.relationshipTypes, input.relationshipType) - ) { - throw new RelationshipTypeNotFoundError( - input.relationshipType, - Object.keys(this.graph.metadata.relationshipTypes), - ) - } - - const id = createLinkId({ - source: input.from, - target: input.to, - ...(input.sourceLocation !== undefined && { sourceLocation: input.sourceLocation }), - }) - if (this.graph.links.some((link) => link.id === id || createLinkId(link) === id)) { - throw new DuplicateLinkError(id) - } - - const link: Link = { - id, - source: input.from, - target: input.to, - ...(input.type !== undefined && { type: input.type }), - ...(input.relationshipType !== undefined && { relationshipType: input.relationshipType }), - ...(input.condition !== undefined && { condition: input.condition }), - ...(input.sourceLocation !== undefined && { sourceLocation: input.sourceLocation }), - } - this.graph.links.push(link) - return link - } - - linkExternal(input: ExternalLinkInput): ExternalLink { - const sourceExists = this.graph.components.some((c) => c.id === input.from) - if (!sourceExists) { - throw createComponentNotFoundError(this.graph.components, input.from) - } - - const duplicate = this.graph.externalLinks.find( - (link) => - link.source === input.from && - link.target.repository === input.target.repository && - link.target.name === input.target.name && - link.type === input.type, - ) - - if (duplicate) { - this.operationWarnings.push({ - code: 'DUPLICATE_LINK_SKIPPED', - message: `Duplicate external link '${input.from}' -> '${input.target.name}' (${input.type ?? 'unspecified'}) skipped`, - source: input.from, - target: input.target.name, - ...(input.type !== undefined && { linkType: input.type }), - ...(input.target.repository !== undefined && { targetRepository: input.target.repository }), - targetName: input.target.name, - }) - - return duplicate - } - - const externalLink: ExternalLink = { - source: input.from, - target: input.target, - ...(input.type !== undefined && { type: input.type }), - ...(input.description !== undefined && { description: input.description }), - ...(input.sourceLocation !== undefined && { sourceLocation: input.sourceLocation }), - } - this.graph.externalLinks.push(externalLink) - return externalLink - } -} diff --git a/packages/riviere-builder/src/features/building/domain/linking/linking-types.ts b/packages/riviere-builder/src/features/building/domain/linking/linking-types.ts deleted file mode 100644 index c6b63cd76..000000000 --- a/packages/riviere-builder/src/features/building/domain/linking/linking-types.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { - ExternalTarget, LinkType, SourceLocation -} from '@living-architecture/riviere-schema' - -/** @riviere-role value-object */ -export interface LinkInput { - from: string - to: string - type?: LinkType - relationshipType?: string - condition?: string - sourceLocation?: SourceLocation -} - -/** @riviere-role value-object */ -export interface ExternalLinkInput { - from: string - target: ExternalTarget - type?: LinkType - description?: string - sourceLocation?: SourceLocation - metadata?: Record -} diff --git a/packages/riviere-builder/src/features/building/domain/riviere-builder.ts b/packages/riviere-builder/src/features/building/domain/riviere-builder.ts deleted file mode 100644 index 7cd861fd7..000000000 --- a/packages/riviere-builder/src/features/building/domain/riviere-builder.ts +++ /dev/null @@ -1,99 +0,0 @@ -import type { RiviereGraph } from '@living-architecture/riviere-schema' -import type { BuilderGraph } from './builder-graph' -import { GraphConstruction } from './construction/graph-construction' -import { GraphEnrichment } from './enrichment/graph-enrichment' -import { GraphLinking } from './linking/graph-linking' -import { GraphInspection } from './inspection/graph-inspection' -import { NearMatch } from './error-recovery/near-match' -import type { BuilderOptions } from './construction/construction-types' -import type { BuilderWarning } from './inspection/inspection-types' -import { - BuildValidationError, - InvalidGraphError, - MissingDomainsError, - MissingSourcesError, -} from './construction/construction-errors' -import { toRiviereGraph } from './inspection/inspection-functions' - -/** @riviere-role domain-service */ -export class RiviereBuilder { - readonly construction: GraphConstruction - readonly enrichment: GraphEnrichment - readonly linking: GraphLinking - readonly inspection: GraphInspection - readonly errorRecovery: NearMatch - readonly graphPath: string - - private readonly graph: BuilderGraph - - private constructor(graph: BuilderGraph, graphPath: string) { - this.graph = graph - this.graphPath = graphPath - const operationWarnings: BuilderWarning[] = [] - this.construction = new GraphConstruction(graph, operationWarnings) - this.enrichment = new GraphEnrichment(graph) - this.linking = new GraphLinking(graph, operationWarnings) - this.inspection = new GraphInspection(graph, operationWarnings) - this.errorRecovery = new NearMatch(graph) - } - - static resume(graph: RiviereGraph, graphPath = ''): RiviereBuilder { - if (!graph.metadata.sources || graph.metadata.sources.length === 0) { - throw new InvalidGraphError('missing sources') - } - - const builderGraph: BuilderGraph = { - version: graph.version, - metadata: { - ...graph.metadata, - sources: graph.metadata.sources, - customTypes: graph.metadata.customTypes ?? {}, - relationshipTypes: graph.metadata.relationshipTypes ?? {}, - }, - components: graph.components, - links: graph.links, - externalLinks: graph.externalLinks ?? [], - } - return new RiviereBuilder(builderGraph, graphPath) - } - - static new(options: BuilderOptions, graphPath = ''): RiviereBuilder { - if (options.sources.length === 0) { - throw new MissingSourcesError() - } - - if (Object.keys(options.domains).length === 0) { - throw new MissingDomainsError() - } - - const graph: BuilderGraph = { - version: '1.0', - metadata: { - ...(options.name !== undefined && { name: options.name }), - ...(options.description !== undefined && { description: options.description }), - sources: options.sources, - domains: options.domains, - customTypes: {}, - relationshipTypes: {}, - }, - components: [], - links: [], - externalLinks: [], - } - - return new RiviereBuilder(graph, graphPath) - } - - serialize(): string { - return JSON.stringify(this.graph, null, 2) - } - - build(): RiviereGraph { - const result = this.inspection.validate() - if (!result.valid) { - const messages = result.errors.map((e) => e.message) - throw new BuildValidationError(messages) - } - return toRiviereGraph(this.graph) - } -} diff --git a/packages/riviere-builder/src/index.ts b/packages/riviere-builder/src/index.ts deleted file mode 100644 index 867bb9ce6..000000000 --- a/packages/riviere-builder/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './features/building/domain' diff --git a/packages/riviere-builder/tsconfig.json b/packages/riviere-builder/tsconfig.json deleted file mode 100644 index 62ebbd946..000000000 --- a/packages/riviere-builder/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "files": [], - "include": [], - "references": [ - { - "path": "./tsconfig.lib.json" - }, - { - "path": "./tsconfig.spec.json" - } - ] -} diff --git a/packages/riviere-builder/tsconfig.lib.json b/packages/riviere-builder/tsconfig.lib.json deleted file mode 100644 index 66780aac6..000000000 --- a/packages/riviere-builder/tsconfig.lib.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "baseUrl": ".", - "rootDir": "src", - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo", - "emitDeclarationOnly": false, - "forceConsistentCasingInFileNames": true, - "types": ["node"] - }, - "include": ["src/**/*.ts"], - "references": [ - { - "path": "../riviere-schema/tsconfig.lib.json" - }, - { - "path": "../riviere-query/tsconfig.lib.json" - } - ], - "exclude": [ - "vite.config.ts", - "vite.config.mts", - "vitest.config.ts", - "vitest.config.mts", - "src/**/*.test.ts", - "src/**/*.spec.ts", - "src/**/*.test.tsx", - "src/**/*.spec.tsx", - "src/**/*.test.js", - "src/**/*.spec.js", - "src/**/*.test.jsx", - "src/**/*.spec.jsx", - "src/__fixtures__/**" - ] -} diff --git a/packages/riviere-builder/tsconfig.spec.json b/packages/riviere-builder/tsconfig.spec.json deleted file mode 100644 index e879c9ad5..000000000 --- a/packages/riviere-builder/tsconfig.spec.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./out-tsc/vitest", - "types": [ - "vitest/globals", - "vitest/importMeta", - "vite/client", - "node", - "vitest" - ], - "forceConsistentCasingInFileNames": true - }, - "include": [ - "vite.config.ts", - "vite.config.mts", - "vitest.config.ts", - "vitest.config.mts", - "src/**/*.test.ts", - "src/**/*.spec.ts", - "src/**/*.test.tsx", - "src/**/*.spec.tsx", - "src/**/*.test.js", - "src/**/*.spec.js", - "src/**/*.test.jsx", - "src/**/*.spec.jsx", - "src/**/*.d.ts", - "src/__fixtures__/**/*.ts" - ], - "references": [ - { - "path": "./tsconfig.lib.json" - } - ] -} diff --git a/packages/riviere-builder/typedoc.json b/packages/riviere-builder/typedoc.json deleted file mode 100644 index 8c1be8f96..000000000 --- a/packages/riviere-builder/typedoc.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "$schema": "https://typedoc.org/schema.json", - "entryPoints": ["./src/index.ts"], - "out": "../../apps/docs/reference/api/generated/riviere-builder", - "plugin": ["typedoc-plugin-markdown", "typedoc-plugin-frontmatter"], - "frontmatterGlobals": { - "pageClass": "reference" - }, - "name": "@living-architecture/riviere-builder", - "readme": "none", - "excludePrivate": true, - "excludeProtected": true, - "excludeInternal": true, - "includeVersion": false, - "disableSources": false, - "sourceLinkTemplate": "https://github.com/NTCoding/living-architecture/blob/main/{path}#L{line}", - "tsconfig": "./tsconfig.lib.json", - "validation": { - "invalidLink": true, - "notExported": false - }, - "hidePageHeader": true, - "hideBreadcrumbs": true -} diff --git a/packages/riviere-builder/use-cases/README.md b/packages/riviere-builder/use-cases/README.md new file mode 100644 index 000000000..457420d8d --- /dev/null +++ b/packages/riviere-builder/use-cases/README.md @@ -0,0 +1,7 @@ +# use-cases + +This library was generated with [Nx](https://nx.dev). + +## Building + +Run `nx build use-cases` to build the library. diff --git a/packages/riviere-builder/use-cases/package.json b/packages/riviere-builder/use-cases/package.json new file mode 100644 index 000000000..36ad15fee --- /dev/null +++ b/packages/riviere-builder/use-cases/package.json @@ -0,0 +1,35 @@ +{ + "name": "@living-architecture/riviere-builder-use-cases", + "version": "0.0.1", + "publishConfig": { + "access": "public" + }, + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "@living-architecture/source": "./src/index.ts", + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./features/*": { + "@living-architecture/source": "./src/features/*.ts", + "types": "./dist/features/*.d.ts", + "import": "./dist/features/*.js", + "default": "./dist/features/*.js" + } + }, + "files": [ + "dist", + "!dist/**/__fixtures__/**", + "!**/*.tsbuildinfo" + ], + "dependencies": { + "@living-architecture/riviere-builder-domain-model": "workspace:*", + "@living-architecture/riviere-schema-published-language": "workspace:*" + } +} diff --git a/packages/riviere-builder/use-cases/project.json b/packages/riviere-builder/use-cases/project.json new file mode 100644 index 000000000..e270a2226 --- /dev/null +++ b/packages/riviere-builder/use-cases/project.json @@ -0,0 +1,18 @@ +{ + "name": "@living-architecture/riviere-builder-use-cases", + "targets": { + "build": { + "executor": "nx:run-commands", + "options": { + "commands": [ + "node ../../../scripts/build-public-exports.mjs . --clean", + "tsc --build tsconfig.lib.json", + "node ../../../scripts/build-public-exports.mjs ." + ], + "cwd": "{projectRoot}", + "parallel": false + }, + "outputs": ["{projectRoot}/dist"] + } + } +} diff --git a/packages/riviere-builder/use-cases/src/__fixtures__/command-test-fixtures.ts b/packages/riviere-builder/use-cases/src/__fixtures__/command-test-fixtures.ts new file mode 100644 index 000000000..310ed9d9d --- /dev/null +++ b/packages/riviere-builder/use-cases/src/__fixtures__/command-test-fixtures.ts @@ -0,0 +1,47 @@ +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, vi } from 'vitest' + +export interface TestContext { + testDir: string + originalCwd: string +} + +export function createTestContext(): TestContext { + return { originalCwd: '', testDir: '' } +} + +export function setupCommandTest(ctx: TestContext): void { + beforeEach(async () => { + ctx.testDir = await mkdtemp(join(tmpdir(), 'riviere-test-')) + ctx.originalCwd = process.cwd() + process.chdir(ctx.testDir) + }) + + afterEach(async () => { + vi.restoreAllMocks() + process.chdir(ctx.originalCwd) + await rm(ctx.testDir, { force: true, recursive: true }) + }) +} + +export async function createGraphWithDomain(testDir: string, domainName: string): Promise { + const graphDir = join(testDir, '.riviere') + await mkdir(graphDir, { recursive: true }) + await writeFile( + join(graphDir, 'graph.json'), + JSON.stringify({ + components: [], + links: [], + metadata: { + domains: { + [domainName]: { description: 'Test domain', systemType: 'domain' }, + }, + sources: [{ repository: 'https://github.com/org/repo' }], + }, + version: '1.0', + }), + 'utf-8', + ) +} diff --git a/packages/riviere-cli/src/features/builder/commands/add-component-input.ts b/packages/riviere-builder/use-cases/src/features/builder/commands/add-component-input.ts similarity index 95% rename from packages/riviere-cli/src/features/builder/commands/add-component-input.ts rename to packages/riviere-builder/use-cases/src/features/builder/commands/add-component-input.ts index a09c7eaba..c1add90dd 100644 --- a/packages/riviere-cli/src/features/builder/commands/add-component-input.ts +++ b/packages/riviere-builder/use-cases/src/features/builder/commands/add-component-input.ts @@ -8,6 +8,7 @@ export interface AddComponentInput { filePath: string graphPathOption?: string lineNumber?: number + columnNumber?: number route?: string apiType?: string httpMethod?: string diff --git a/packages/riviere-builder/use-cases/src/features/builder/commands/add-component-result.ts b/packages/riviere-builder/use-cases/src/features/builder/commands/add-component-result.ts new file mode 100644 index 000000000..3abf8c7b9 --- /dev/null +++ b/packages/riviere-builder/use-cases/src/features/builder/commands/add-component-result.ts @@ -0,0 +1,19 @@ +/** @riviere-role command-use-case-result-value */ +export type AddComponentErrorCode = + | 'VALIDATION_ERROR' + | 'GRAPH_NOT_FOUND' + | 'DOMAIN_NOT_FOUND' + | 'CUSTOM_TYPE_NOT_FOUND' + | 'DUPLICATE_COMPONENT' + +/** @riviere-role command-use-case-result */ +export type AddComponentResult = + | { + success: true + componentId: string + } + | { + success: false + code: AddComponentErrorCode + message: string + } diff --git a/packages/riviere-builder/use-cases/src/features/builder/commands/add-component.spec.ts b/packages/riviere-builder/use-cases/src/features/builder/commands/add-component.spec.ts new file mode 100644 index 000000000..8854de7b2 --- /dev/null +++ b/packages/riviere-builder/use-cases/src/features/builder/commands/add-component.spec.ts @@ -0,0 +1,170 @@ +import { writeFile, mkdir } from 'node:fs/promises' +import { join } from 'node:path' +import { describe, expect, it } from 'vitest' +import { AddComponent } from './add-component' +import { RiviereBuilderRepository } from '../data-access/riviere-builder/riviere-builder-repository' +import type { AddComponentErrorCode } from './add-component-result' +import { + type TestContext, + createTestContext, + setupCommandTest, + createGraphWithDomain, +} from '../../../__fixtures__/command-test-fixtures' + +describe('addComponent command', () => { + const ctx: TestContext = createTestContext() + setupCommandTest(ctx) + + const baseInput = { + componentType: 'UI', + name: 'TestComponent', + domain: 'test-domain', + module: 'test-module', + repository: 'test-repo', + filePath: '/path/to/file.ts', + } + + function inputWithGraphPath(overrides: Partial = {}) { + return { + ...baseInput, + graphPathOption: join(ctx.testDir, '.riviere', 'graph.json'), + route: '/test', + ...overrides, + } + } + + function failureShape(code: AddComponentErrorCode) { + return { + success: false as const, + code, + } + } + + describe('component type validation', () => { + it.each([ + ['invalid string', 'INVALID'], + ['empty', ''], + ['whitespace', ' '], + ['special chars', 'UI