Skip to content

Phase 1: add adaptive documentation planning foundation - #2

Open
fajarnugraha37 wants to merge 15 commits into
mainfrom
feat/phase1-adaptive-planning
Open

Phase 1: add adaptive documentation planning foundation#2
fajarnugraha37 wants to merge 15 commits into
mainfrom
feat/phase1-adaptive-planning

Conversation

@fajarnugraha37

@fajarnugraha37 fajarnugraha37 commented Jul 17, 2026

Copy link
Copy Markdown
Owner

What changed

  • upgrade the normalized configuration model to version 3 while retaining v1 services and v2 component compatibility
  • separate deployable components from domain/module/flow/platform/catalog documentation units
  • add composable capability packs, configurable documentation views, evidence boundaries, and catalog shard policy
  • add deterministic component discovery with stable source hashes, evidence matches, inferred units, binary/size/symlink guards, and explicit unknowns
  • add adaptive component and whole-system plans with include/skip/defer decisions and output-collision handling
  • persist discovery.json, component plan.json, and system plan.json, and snapshot them into whole-system aggregation workspaces
  • inject bounded adaptive context and complete artifact references into generation/update prompts and persistent instructions
  • add wikiforge discover, wikiforge plan --explain, and wikiforge config migrate
  • preserve last-successful source/docs/discovery/plan checkpoints when generation fails
  • update the Draft 2020-12 schema, examples, release notes, architecture docs, and generated WikiForge documentation
  • harden CI with no-cache tests and vet on Ubuntu/Windows/macOS plus the race detector on Linux

Compatibility boundary

Phase 1 introduces the adaptive discovery and planning foundation. The existing fixed profile renderer and structural validator intentionally remain the compatibility renderer; hierarchical page materialization and physical catalog sharding belong to Phase 2.

Verification

Local and GitHub-hosted verification covers:

  • go test -count=1 ./...
  • go vet ./...
  • go test -race -count=1 ./...
  • executable build and six-target Linux/Windows/macOS cross-compilation
  • randomized test order
  • v1/v2 migration and reloadable v3 output
  • schema/example validation and registry drift
  • deterministic discovery and planning
  • all registered capability-pack outcomes
  • monolith, modular-monolith, microservice, library, framework, and infrastructure fixtures
  • end-to-end fake-runner generation, update no-op, repair, graph, state, monorepo serialization, and immutable system snapshots
  • prompt placeholder resolution and bounded prompt summaries
  • workflow, documentation-link, front-matter, secret, whitespace, and artifact checks

A live model-provider-backed OpenWiki generation is not claimed because no provider credential was supplied. The OpenWiki executable boundary, prompt transport, streaming, timeout, retry, clarification detection, orchestration, and fake-runner end-to-end paths are automated and verified.

Copy link
Copy Markdown
Owner Author

WIKIFORGE_PHASE1_PATCH_PART 01/19
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 38d00c9db68f31d43aad486809f7414acaee936f..5939a66e777c5fd6781e40d1c99b2594dc2454b9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -13,7 +13,8 @@ jobs:
- uses: actions/setup-go@v5
with:
go-version: '1.23.x'

  •  - run: go test ./...
    
  •  - run: go test -count=1 ./...
    
  •  - run: go vet ./...
     - if: runner.os == 'Linux'
    
  •    run: go test -race ./...
    
  •    run: go test -race -count=1 ./...
     - run: go build ./cmd/wikiforge
    

diff --git a/.github/workflows/export-source.yml b/.github/workflows/export-source.yml
deleted file mode 100644
index 4ddb99697641e3fb9d826466bc558540190e9485..0000000000000000000000000000000000000000
--- a/.github/workflows/export-source.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-name: export-source
-on:

  • pull_request:
  • branches: [main]

-jobs:

  • export:
  • runs-on: ubuntu-latest
  • steps:
  •  - uses: actions/checkout@v4
    
  •  - uses: actions/upload-artifact@v4
    
  •    with:
    
  •      name: wikiforge-source
    
  •      path: |
    
  •        .
    
  •        !.git/**
    
  •      include-hidden-files: true
    
  •      retention-days: 1
    

diff --git a/BUILD-VERIFICATION.md b/BUILD-VERIFICATION.md
index f8dd3e4ff387f1ecca04b31181f0d244478d745c..f70e49f2b7dc1ad5d86a599500cab5b439061dda 100644
--- a/BUILD-VERIFICATION.md
+++ b/BUILD-VERIFICATION.md
@@ -1,12 +1,18 @@

Build Verification

-Release: 1.2.3
+Build: 1.3.0 — Phase 1 adaptive-planning foundation

Verified in this build environment

-- go test ./...
-- go test -race ./...
+- go test -count=1 ./...
+- go test -race -count=1 ./...

  • go vet ./...
    +- Configuration v3 normalization with v1/v2 compatibility and reloadable migration output.
    +- Deterministic discovery manifests with include/exclude globs, file-size and binary guards, symlink avoidance, nested module/domain inference, BPMN flow inference, and configured-unit precedence.
    +- Adaptive planner coverage for every registered capability pack, all documentation views, documentation-unit outputs, explicit collision decisions, catalog shard policy, and whole-system aggregation.
    +- CLI coverage for discover, plan --explain, config migrate, unknown-component failures, and deterministic artifact persistence.
    +- Last-successful source, documentation, discovery, and plan checkpoints survive failed generation attempts.
    +- Published v3 YAML examples parse through WikiForge and validate against the Draft 2020-12 JSON Schema.
  • OpenWiki executable runner contract tests:
    • --init --print;
    • --update --print;
      diff --git a/DOCUMENTATION-CATALOG.md b/DOCUMENTATION-CATALOG.md
      index 285dace46fc3439d58c78aae5f619ddf13c9c648..e379c63b707fe2d7eb6800a489f1d3918bfdf165 100644
      --- a/DOCUMENTATION-CATALOG.md
      +++ b/DOCUMENTATION-CATALOG.md
      @@ -1,6 +1,6 @@
      -# WikiForge 1.2.3 Documentation Catalog
      +# WikiForge Compatibility Renderer Documentation Catalog

-This document lists the specialized Markdown contracts enforced by WikiForge 1.2.3. Core profile documents remain in addition to these pages.
+This document lists the fixed Markdown contracts enforced by the backward-compatible profile renderer. Phase 1 additionally produces adaptive discovery and planning artifacts; Phase 2 will render the hierarchical plan directly. Core profile documents remain in addition to these pages.

Component specialized pages

diff --git a/PHASE-1-ADAPTIVE-PLANNING.md b/PHASE-1-ADAPTIVE-PLANNING.md
new file mode 100644
index 0000000000000000000000000000000000000000..fdd1bb1b23bed96b84392c410f17e20a2fffd251
--- /dev/null
+++ b/PHASE-1-ADAPTIVE-PLANNING.md
@@ -0,0 +1,112 @@
+# Phase 1: Adaptive Planning Foundation
+
+Phase 1 changes WikiForge's planning model without replacing the existing validated profile renderer. The compatibility renderer remains active; discovery and planning artifacts now provide the bounded inputs required by the hierarchical renderer planned for Phase 2.
+
+## Implemented architecture
+
+```text
+ComponentConfig v3

  • -> deterministic source discovery
  • -> DiscoveryManifest
  •  -> composable profile + explicit + discovered capability packs
    
  •    -> DocumentationUnit set
    
  •      -> adaptive DocumentationPlan
    
  •        -> persisted artifacts
    
  •          -> component/system prompts and immutable aggregation snapshots
    

+ + +## Configuration v3 + +Version 3 adds: + +- `components[].owners`, `capabilities`, and `packs`; +- top-level `documentationUnits`; +- `documentation.views`; +- `documentation.catalogs.shardBy` and `maximumRowsPerPage`; +- `documentation.evidence.include`, `exclude`, and `maxFileSizeBytes`. + +Version 1 services and version 2 components are accepted and normalized in memory to version 3. `wikiforge config migrate` emits a normalized version 3 JSON configuration. + +## Documentation units + +A documentation unit is not assumed to be deployable. Supported kinds are: + +- domain; +- subdomain; +- bounded context; +- component; +- module; +- flow; +- platform area; +- catalog. + +Configured units preserve source roots, related units, output path, owners, capabilities, and criticality. Discovery can infer module/domain units from conventional roots and flow units from BPMN files. Explicit units take precedence and prevent duplicate inferred units. + +## Capability packs + +The planner composes: + +1. base profile packs; +2. explicit component packs; +3. packs discovered from source evidence. + +Every supported pack has a canonical Phase 1 planning outcome. A pack is included, skipped with a reason, or deferred because its required view is disabled. No registered pack is silent or dead configuration. + +## Deterministic discovery + +Discovery: + +- walks only the configured component scope; +- applies configurable include/exclude globs; +- skips excluded directories, oversized files, binary files, and symbolic links; +- records evidence paths per capability pack; +- calculates a stable source hash; +- emits no timestamps or map-order-dependent content. + +Artifacts are written to: + +text
+.wikiforge/components//discovery.json
+.wikiforge/components//plan.json
+.wikiforge/system/plan.json
+ + +The whole-system aggregation workspace snapshots component discovery/plan files and `sources/system-plan.json` next to generated component wiki snapshots. + +## CLI + +text
+wikiforge discover [--config PATH] [--component ID]
+wikiforge plan [--config PATH] [--component ID] [--skip-system] [--explain]
+wikiforge config migrate [--config PATH] [--output PATH] [--force]
+```
+
+plan --explain reports selected packs, planned page kinds, collection shard policy, and every skip/defer reason. `discover` and `plan` persist deterministic artifacts.
+
+## Runtime integration
+
+Component and system prompts receive:
+
+- selected capability packs;
+- documentation units;
+- future adaptive page paths;
+- include/skip/defer decisions.
+
+Persistent `INSTRUCTIONS.md` files carry the same context. The current renderer still owns its legacy profile page for each phase, preventing Phase 1 from silently changing the physical wiki contract before Phase 2 validation and migration are available.
+
+## Verification scope
+
+Automated coverage includes:
+
+- v1/v2 to v3 normalization and reloadable migration output;
+- documentation-unit reference, path, pack, and view validation;
+- deterministic discovery and source-hash changes;
+- evidence include/exclude, root-level `**` globs, binary, size, and symlink guards;
+- configured capability units, inferred module/domain units, BPMN flow inference, and deduplication;
+- canonical planning outcome for every capability pack;
+- disabled-view deferral and explicit reasoning;
+- different relevant plans for monolith, modular monolith, microservice, library, framework, and infrastructure fixtures;
+- system-plan aggregation;
+- CLI artifact creation and `--explain` output;
+- prompt placeholder resolution;
+- end-to-end generation, immutable system snapshots, no-op update behavior, validation, graph export, and monorepo serialization.
diff --git a/README.md b/README.md
index e002123831a787b41a595e4a8811a4f5a1a955dd..28504f670391829b0e31e6bc55aae32e43a2249e 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@ WikiForge intentionally does not parse programming languages, frameworks, Ia

Supported repository scenarios

-WikiForge 1.2.3 is component-centric. A component may be an entire repository or a scoped directory inside a monorepo.
+WikiForge is component-centric. A component may be an entire repository or a scoped directory inside a monorepo.

Scenario Component type Documentation profile
@@ -55,8 +55,13 @@ Different Git repositories may run concurrently. Components sharing the same Git
  • Native cross-platform Go CLI.
  • YAML or JSON configuration.
    -- Config v2 component model with backward-compatible v1 services migration.
    +- Config v3 adaptive-planning model with backward-compatible v1 services and v2 component migration.
  • Repository-root and monorepo-scoped components.
    +- Explicit documentation units for domains, subdomains, bounded contexts, modules, flows, platform areas, and catalogs.
    +- Composable capability packs selected from profile defaults, explicit configuration, and deterministic source discovery.
    +- Deterministic discovery manifests and adaptive documentation plans persisted under .wikiforge/components/<id>/.
    +- Configurable domain/component/flow/catalog/platform/engineering/operations views, evidence include/exclude rules, and catalog shard policy.
    +- discover, plan --explain, and config migrate CLI workflows.
  • Profile-specific phased generation for seven repository classes.
  • Bounded parallelism across repositories and automatic serialization within a monorepo.
  • Persistent profile-aware openwiki/INSTRUCTIONS.md contracts.
    @@ -80,9 +85,11 @@ Different Git repositories may run concurrently. Components sharing the same Git
  • JSONL knowledge graph export for document links and standardized relationship tables.
  • End-to-end tests covering every profile, mixed repository types, monorepo scopes, whole-system generation, and same-repository serialization.

+The detailed Phase 1 architecture, invariants, CLI behavior, and verification scope are documented in PHASE-1-ADAPTIVE-PLANNING.md.
+

Live progress and long-running phases

-WikiForge 1.2.3 no longer appears frozen while OpenWiki is working. Every generation displays a line-based progress bar with the component, phase ID, completed percentage, step number, status, and elapsed time. The percentage is based on completed deterministic WikiForge steps; it does not invent model-token progress inside a single OpenWiki call.
+WikiForge does not appear frozen while OpenWiki is working. Every generation displays a line-based progress bar with the component, phase ID, completed percentage, step number, status, and elapsed time. The percentage is based on completed deterministic WikiForge steps; it does not invent model-token progress inside a single OpenWiki call.

Example:

@@ -128,14 +135,14 @@ Provider credentials required by the selected OpenWiki model must be available a
Windows:

-Expand-Archive .\wikiforge-1.2.3-windows-amd64.zip
-cd .\wikiforge-1.2.3-windows-amd64
+Expand-Archive .\wikiforge-<version>-windows-amd64.zip
+cd .\wikiforge-<version>-windows-amd64

Linux/macOS:

-unzip wikiforge-1.2.3-linux-amd64.zip
+unzip wikiforge-<version>-linux-amd64.zip
cd linux-amd64

@@ -179,12 +186,15 @@ $env:OPENWIKI_MODEL_ID = "cheap-code-model"
./wikiforge profiles


-### 6. Preview the exact phase plan
+### 6. Discover capabilities and preview the adaptive plan

```bash
-./wikiforge plan
+./wikiforge discover
+./wikiforge plan --explain

Copy link
Copy Markdown
Owner Author

WIKIFORGE_PHASE1_PATCH_PART 02/19
+Discovery and planning are deterministic for the same normalized configuration and source content. They write .wikiforge/components/<component-id>/discovery.json and plan.json. The existing profile renderer consumes this context while Phase 2 introduces the hierarchical physical layout.
+

7. Generate all component wikis and the whole-system wiki

@@ -211,7 +221,9 @@ WikiForge hashes each configured component scope independently. A change elsewhe
wikiforge init [--config wikiforge.yaml] [--force]
wikiforge doctor [--config wikiforge.yaml]
wikiforge profiles
-wikiforge plan [--config wikiforge.yaml] [--component ID] [--skip-system]
+wikiforge config migrate [--config wikiforge.yaml] [--output wikiforge.v3.json] [--force]
+wikiforge discover [--config wikiforge.yaml] [--component ID]
+wikiforge plan [--config wikiforge.yaml] [--component ID] [--skip-system] [--explain]
wikiforge generate [--config wikiforge.yaml] [--component ID] [--skip-system] [--resume]
wikiforge update [--config wikiforge.yaml] [--component ID] [--skip-system]
wikiforge resume [--config wikiforge.yaml]
@@ -234,6 +246,9 @@ components:
    group: commerce
    tags: [core, deployable]
    dependsOn: [shared-contracts]
+    owners: [commerce-team]
+    capabilities: [order-management, pricing]
+    packs: [workflow, messaging, database]

Fields:
@@ -247,10 +262,48 @@ Fields:

  • includeInSystem: whether its generated wiki is included in whole-system aggregation;
  • group: optional logical grouping;
  • tags: optional classification;
    -- dependsOn: optional declared component dependencies included in the system manifest.
    +- dependsOn: optional declared component dependencies included in the system manifest;
    +- owners: optional ownership hints used by planning and future catalog sharding;
    +- capabilities: business capabilities that become configured domain documentation units;
    +- packs: capability packs composed with profile defaults and source-discovered packs.

scope must be relative and cannot escape the repository.

+### Documentation units
+
+A documentation unit is deliberately separate from a deployable component:
+
+```yaml
+documentationUnits:

    • id: order-management
  • component: commerce-core
  • kind: domain
  • sourceRoots: [modules/order, workflows/order]
  • output: domains/order-management
  • owners: [commerce-team]
  • capabilities: [order-management]
  • criticality: high
    • id: submit-order
  • component: commerce-core
  • kind: flow
  • sourceRoots: [workflows/order/submit-order.bpmn]
  • relatedUnits: [order-management]
  • output: flows/submit-order
    +```

+Supported kinds are domain, subdomain, bounded-context, component, module, flow, platform, and catalog. Source roots and output paths are normalized relative paths and cannot escape the component scope.
+
+### Adaptive planning
+
+The planner combines three inputs:
+
+1. capability packs required by the component profile;
+2. explicit component packs;
+3. capability evidence discovered from eligible source files.
+
+It records included, skipped, and deferred decisions rather than forcing every concern into a fixed page set. The Phase 1 plan describes the future hierarchical pages while the existing renderer remains backward-compatible.
+

Documentation profiles

Application

@@ -359,7 +412,7 @@ A neutral fallback that does not force service or application terminology.

Specialized documentation pack

-Version 1.2.3 provides deterministic first-class documentation for runtime configuration, integrations, interfaces, messaging, jobs, business behaviour, traffic, request processing, security, concurrency, asynchronous work, context propagation, databases, cryptography, and files.
+The backward-compatible profile renderer provides deterministic first-class documentation for runtime configuration, integrations, interfaces, messaging, jobs, business behaviour, traffic, request processing, security, concurrency, asynchronous work, context propagation, databases, cryptography, and files.

The following related subjects remain intentionally merged:

@@ -472,6 +525,10 @@ mermaid:

.wikiforge/
├── state.json
+├── components/
+│   └── <component-id>/
+│       ├── discovery.json
+│       └── plan.json
├── validation/
│   ├── <component-id>.json
│   └── system.json
@@ -500,7 +557,7 @@ services:
    enabled: true

-are normalized to microservice components using the application profile. New configurations should use version: 2 and components.
+are normalized to microservice components using the application profile. Version 2 component configurations are also accepted and normalized in memory to version 3. New configurations should use version: 3, components, and optional documentationUnits. Use wikiforge config migrate to emit a normalized version 3 JSON file.

Security

diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index fd4f95ef83934a19ef3cb90ebe9f2777344db301..01fc4528c5bcd7de5494488c6fae9baddbc77f75 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -1,4 +1,31 @@
-# WikiForge 1.2.3 Release Notes
+# WikiForge 1.3.0 Release Notes
+
+## Adaptive planning foundation
+
+WikiForge now separates deployable components from documentation units and builds a deterministic discovery manifest plus adaptive documentation plan before generation. Configuration version 3 adds documentation views, composable capability packs, evidence boundaries, catalog shard policy, ownership/capability metadata, and explicit domain/module/flow/platform/catalog units.
+
+New commands:
+
+text +wikiforge discover +wikiforge plan --explain +wikiforge config migrate +
+
+Version 1 and 2 configurations remain accepted and are normalized to version 3. Existing fixed profile phases remain the compatibility renderer during Phase 1; prompts and system aggregation now consume the adaptive plan so Phase 2 can change the physical layout without discarding the orchestration core.
+
+## Correctness and safety
+
+- all registered capability packs have include, skip, or defer outcomes;
+- output-path collisions are explicit planning decisions rather than silent drops;
+- discovery is deterministic, bounded by configurable evidence rules, and does not follow symbolic links;
+- discovery and plan artifacts are persisted and included in whole-system snapshots;
+- failed runs preserve the last-successful source, documentation, discovery, and plan checkpoint;
+- JSON Schema, examples, CLI workflows, profile fixtures, orchestration, race detection, vet, build, and cross-platform CI are verified.
+
+---
+
+## WikiForge 1.2.3

GitHub Releases and downloadable binaries

diff --git a/RELEASING.md b/RELEASING.md
index a04ac63d5bc2fda7060db9dcb03ba74322fa69e5..e0cbdab6482a5e5491adb963037cb64f92335c2e 100644
--- a/RELEASING.md
+++ b/RELEASING.md
@@ -8,11 +8,11 @@ From an up-to-date main branch:

git pull --ff-only
-git tag v1.2.3
-git push origin v1.2.3
+git tag v<semver>
+git push origin v<semver>

-Use the next semantic version after v1.2.3 once that tag already exists.
+Replace <semver> with the intended release version, for example 1.3.0. Never move or reuse an existing release tag.

The release workflow will:

@@ -34,8 +34,8 @@ The workflow also supports manual dispatch with an existing v-prefixed tag. It i

Verify locally before tagging

-go test ./...
-go test -race ./...
+go test -count=1 ./...
+go test -race -count=1 ./...
go vet ./...
goreleaser release --snapshot --clean

diff --git a/examples/wikiforge.yaml b/examples/wikiforge.yaml
index df2d27f19937fb229771c3e5db0e055ace99ad09..e2e80944b09f36ca5f7da794177638de64f1259b 100644
--- a/examples/wikiforge.yaml
+++ b/examples/wikiforge.yaml
@@ -1,13 +1,9 @@
-version: 2
+version: 3
workspace: .

openwiki:

  • Pinned for reproducible runs. Replace with your approved command/model.

    command: npx
  • args:
    • --yes
    • openwiki@0.2.0
    • code
  • args: [--yes, openwiki@0.2.0, code]
    modelId: ""
    timeoutMinutes: 60
    environment:
    @@ -15,8 +11,6 @@ openwiki:
    OPENWIKI_PROVIDER_RETRY_ATTEMPTS: "3"

execution:

  • Different repositories may run concurrently. Components sharing one

  • repository are automatically serialized, which is safe for monorepos.

    parallelComponents: 2
    maxProcessRetries: 2
    maxRepairRounds: 2
    @@ -25,99 +19,67 @@ execution:
    documentation:
    language: English
    minimumQualityScore: 85
  • 0 = use the selected component profile's built-in contract.

    minimumPages: 0
    requireFrontMatter: true
    requireSourceReferences: true
    validateSourcePaths: true
    requireMermaid: true
  • 0 = use the selected component profile's built-in minimum.

    minimumMermaidBlocks: 0
  • allowedDiagramTypes:
    • flowchart
    • sequenceDiagram
    • stateDiagram-v2
    • erDiagram
    • classDiagram
    • architecture-beta
    • gitGraph
    • mindmap
  • allowedDiagramTypes: [flowchart, sequenceDiagram, stateDiagram-v2, erDiagram, classDiagram, architecture-beta, gitGraph, mindmap]
  • Phase 1 planning views. Phase 2 will render this hierarchy directly.

  • views: [system, domain, component, flow, catalog, platform, engineering, operations]
  • catalogs:
  • shardBy: [domain, owner]
  • maximumRowsPerPage: 150
  • evidence:
  • include: ["**"]
  • exclude: [.git/, .wikiforge/, openwiki/, vendor/, node_modules/, dist/, build/, target/, generated/, .wikiforge-prompt-*.md, "/*.bin"]
  • maxFileSizeBytes: 2097152

mermaid:

  • render = parse every Mermaid block with the pinned official CLI.

  • basic = offline structural checks only.

  • off = do not validate Mermaid.

    mode: render
    command: npx
  • args:
    • --yes
    • "@mermaid-js/mermaid-cli@11.12.0"
    • -i
    • "{input}"
    • -o
    • "{output}"
    • --quiet
  • args: [--yes, "@mermaid-js/mermaid-cli@11.12.0", -i, "{input}", -o, "{output}", --quiet]
    timeoutSeconds: 90

-# A component is any independently documented repository scope. Multiple
-# components may share one repository by using different relative scopes.
components:

  • Conventional microservice repository.

    • id: order-service
  • type: microservice
  • repository: ./repositories/order-service
  • enabled: false
  • group: commerce
  • tags: [order, deployable]
  • dependsOn: [shared-contracts]
  • Entire monolith repository.

    • id: legacy-commerce
  • type: monolith
  • repository: ./repositories/legacy-commerce
  • enabled: false
  • Modular monolith with module-aware documentation.

    • id: commerce-core
      type: modular-monolith
      repository: ./repositories/commerce-core
  • enabled: false
  • Multiple scoped components inside one monorepo.

    • id: catalog-app
  • type: microservice
  • repository: ./repositories/platform-monorepo
  • scope: apps/catalog
  • enabled: false
  • enabled: true
  • owners: [commerce-team]
  • capabilities: [order-management, pricing, fulfilment]
  • Explicit packs are composed with profile defaults and discovered packs.

  • packs: [workflow, messaging, database, migrations]
  • id: shared-runtime
    type: framework
    repository: ./repositories/platform-monorepo
    scope: packages/runtime
    enabled: false
  • owners: [platform-team]
  • packs: [concurrency, telemetry]
    • id: shared-contracts
  • type: contracts
  • repository: ./repositories/platform-monorepo
  • scope: contracts
  • enabled: false
  • Infrastructure/IaC/GitOps repository.

    • id: production-infrastructure
  • type: iac
  • repository: ./repositories/production-infrastructure
  • enabled: false
    +documentationUnits:
    • id: order-management
  • component: commerce-core
  • kind: domain
  • sourceRoots: [modules/order, workflows/order]
  • output: domains/order-management

Copy link
Copy Markdown
Owner Author

WIKIFORGE_PHASE1_PATCH_PART 03/19

  • owners: [commerce-team]
  • capabilities: [order-management]
  • criticality: high
  • Shared configuration repository.

    • id: platform-configuration
  • type: configuration
  • repository: ./repositories/platform-configuration
  • enabled: false
    • id: submit-order
  • component: commerce-core
  • kind: flow
  • sourceRoots: [workflows/order/submit-order.bpmn]
  • relatedUnits: [order-management]
  • output: flows/submit-order

system:
enabled: true
id: enterprise-system
title: Enterprise System
output: ./enterprise-wiki

  • Optional human-authored facts copied into the aggregation workspace.

    factsPath: ./facts
    diff --git a/internal/assets/prompts/common/base.md b/internal/assets/prompts/common/base.md
    index ab179f6220177dd40e1330e9cdb03766d1613fcf..2dc03903b073f47f8ca5031e9a00281b1747d18e 100644
    --- a/internal/assets/prompts/common/base.md
    +++ b/internal/assets/prompts/common/base.md
    @@ -24,3 +24,23 @@ Mandatory operating rules:
    Output language: {{LANGUAGE}}
    Target component/system: {{TARGET_ID}}
    Documentation root: openwiki/
    +Adaptive planning context for this target:

+Full deterministic artifacts:
+
+- discovery: {{DISCOVERY_ARTIFACT}}
+- plan: {{PLAN_ARTIFACT}}
+
+Selected capability packs:
+{{ADAPTIVE_PACKS}}
+
+Documentation units:
+{{DOCUMENTATION_UNITS}}
+
+Future adaptive pages:
+{{ADAPTIVE_PAGES}}
+
+Planning decisions:
+{{PLAN_DECISIONS}}
+
+Use this context to focus evidence collection and preserve domain, component, flow, catalog, platform, engineering, and operations boundaries. The current compatibility renderer must still honor the owned page contract for the active phase.
diff --git a/internal/assets/templates/instructions.md b/internal/assets/templates/instructions.md
index c177d64c5f4af87a901b9d429bc6086dde7ae4a1..1e29f3c7adf6b7605fe7ddddfc4fc00540c5896d 100644
--- a/internal/assets/templates/instructions.md
+++ b/internal/assets/templates/instructions.md
@@ -48,4 +48,27 @@ Classify important information as Verified, Derived, Unknown, or **C

  • Stay within the configured repository scope unless a referenced neighbour is required to explain an interaction.

Documentation language: {{LANGUAGE}}
+## Adaptive planning context
+
+Full deterministic artifacts:
+
+- discovery: {{DISCOVERY_ARTIFACT}}
+- plan: {{PLAN_ARTIFACT}}
+
+Selected capability packs:
+
+{{ADAPTIVE_PACKS}}
+
+Documentation units:
+
+{{DOCUMENTATION_UNITS}}
+
+Planned adaptive pages for the future hierarchical layout:
+
+{{ADAPTIVE_PAGES}}
+
+Planning decisions:
+
+{{PLAN_DECISIONS}}
+

diff --git a/internal/assets/templates/system-instructions.md b/internal/assets/templates/system-instructions.md
index 02a096c4cab1bc657b5e3884c8e9c061ceb2f9a9..2e23c79bec71d9d79c6c66efe22d524807b3692f 100644
--- a/internal/assets/templates/system-instructions.md
+++ b/internal/assets/templates/system-instructions.md
@@ -11,4 +11,22 @@ The wiki must help humans and LLM agents trace capabilities, deployables, module

Documentation language: {{LANGUAGE}}
System identifier: {{TARGET_ID}}
+## Adaptive system plan
+
+Selected capability packs aggregated from component plans:
+
+{{ADAPTIVE_PACKS}}
+
+Cross-component documentation units:
+
+{{DOCUMENTATION_UNITS}}
+
+Planned hierarchical system pages:
+
+{{ADAPTIVE_PAGES}}
+
+Planning decisions:
+
+{{PLAN_DECISIONS}}
+

diff --git a/internal/assets/templates/wikiforge.yaml b/internal/assets/templates/wikiforge.yaml
index df2d27f19937fb229771c3e5db0e055ace99ad09..e2e80944b09f36ca5f7da794177638de64f1259b 100644
--- a/internal/assets/templates/wikiforge.yaml
+++ b/internal/assets/templates/wikiforge.yaml
@@ -1,13 +1,9 @@
-version: 2
+version: 3
workspace: .

openwiki:

  • Pinned for reproducible runs. Replace with your approved command/model.

    command: npx
  • args:
    • --yes
    • openwiki@0.2.0
    • code
  • args: [--yes, openwiki@0.2.0, code]
    modelId: ""
    timeoutMinutes: 60
    environment:
    @@ -15,8 +11,6 @@ openwiki:
    OPENWIKI_PROVIDER_RETRY_ATTEMPTS: "3"

execution:

  • Different repositories may run concurrently. Components sharing one

  • repository are automatically serialized, which is safe for monorepos.

    parallelComponents: 2
    maxProcessRetries: 2
    maxRepairRounds: 2
    @@ -25,99 +19,67 @@ execution:
    documentation:
    language: English
    minimumQualityScore: 85
  • 0 = use the selected component profile's built-in contract.

    minimumPages: 0
    requireFrontMatter: true
    requireSourceReferences: true
    validateSourcePaths: true
    requireMermaid: true
  • 0 = use the selected component profile's built-in minimum.

    minimumMermaidBlocks: 0
  • allowedDiagramTypes:
    • flowchart
    • sequenceDiagram
    • stateDiagram-v2
    • erDiagram
    • classDiagram
    • architecture-beta
    • gitGraph
    • mindmap
  • allowedDiagramTypes: [flowchart, sequenceDiagram, stateDiagram-v2, erDiagram, classDiagram, architecture-beta, gitGraph, mindmap]
  • Phase 1 planning views. Phase 2 will render this hierarchy directly.

  • views: [system, domain, component, flow, catalog, platform, engineering, operations]
  • catalogs:
  • shardBy: [domain, owner]
  • maximumRowsPerPage: 150
  • evidence:
  • include: ["**"]
  • exclude: [.git/, .wikiforge/, openwiki/, vendor/, node_modules/, dist/, build/, target/, generated/, .wikiforge-prompt-*.md, "/*.bin"]
  • maxFileSizeBytes: 2097152

mermaid:

  • render = parse every Mermaid block with the pinned official CLI.

  • basic = offline structural checks only.

  • off = do not validate Mermaid.

    mode: render
    command: npx
  • args:
    • --yes
    • "@mermaid-js/mermaid-cli@11.12.0"
    • -i
    • "{input}"
    • -o
    • "{output}"
    • --quiet
  • args: [--yes, "@mermaid-js/mermaid-cli@11.12.0", -i, "{input}", -o, "{output}", --quiet]
    timeoutSeconds: 90

-# A component is any independently documented repository scope. Multiple
-# components may share one repository by using different relative scopes.
components:

  • Conventional microservice repository.

    • id: order-service
  • type: microservice
  • repository: ./repositories/order-service
  • enabled: false
  • group: commerce
  • tags: [order, deployable]
  • dependsOn: [shared-contracts]
  • Entire monolith repository.

    • id: legacy-commerce
  • type: monolith
  • repository: ./repositories/legacy-commerce
  • enabled: false
  • Modular monolith with module-aware documentation.

    • id: commerce-core
      type: modular-monolith
      repository: ./repositories/commerce-core
  • enabled: false
  • Multiple scoped components inside one monorepo.

    • id: catalog-app
  • type: microservice
  • repository: ./repositories/platform-monorepo
  • scope: apps/catalog
  • enabled: false
  • enabled: true
  • owners: [commerce-team]
  • capabilities: [order-management, pricing, fulfilment]
  • Explicit packs are composed with profile defaults and discovered packs.

  • packs: [workflow, messaging, database, migrations]
  • id: shared-runtime
    type: framework
    repository: ./repositories/platform-monorepo
    scope: packages/runtime
    enabled: false
  • owners: [platform-team]
  • packs: [concurrency, telemetry]
    • id: shared-contracts
  • type: contracts
  • repository: ./repositories/platform-monorepo
  • scope: contracts
  • enabled: false
  • Infrastructure/IaC/GitOps repository.

    • id: production-infrastructure
  • type: iac
  • repository: ./repositories/production-infrastructure
  • enabled: false
    +documentationUnits:
    • id: order-management
  • component: commerce-core
  • kind: domain
  • sourceRoots: [modules/order, workflows/order]
  • output: domains/order-management
  • owners: [commerce-team]
  • capabilities: [order-management]
  • criticality: high
  • Shared configuration repository.

    • id: platform-configuration
  • type: configuration
  • repository: ./repositories/platform-configuration
  • enabled: false
    • id: submit-order
  • component: commerce-core
  • kind: flow
  • sourceRoots: [workflows/order/submit-order.bpmn]
  • relatedUnits: [order-management]
  • output: flows/submit-order

system:
enabled: true
id: enterprise-system
title: Enterprise System
output: ./enterprise-wiki

  • Optional human-authored facts copied into the aggregation workspace.

    factsPath: ./facts
    diff --git a/internal/cli/cli.go b/internal/cli/cli.go
    index 771f78a15c3f707a1f7e5498d3d3343822a9cd9c..3f0260f58860f28545b1009ffb5a54a949c71e54 100644
    --- a/internal/cli/cli.go
    +++ b/internal/cli/cli.go
    @@ -22,7 +22,7 @@ import (
    "github.com/example/wikiforge/internal/validation"
    )

-const Version = "1.2.3"
+const Version = "1.3.0"

type CLI struct {
Out io.Writer
@@ -49,6 +49,10 @@ func (c CLI) Run(ctx context.Context, args []string) int {
return 0
case "profiles", "types":
return c.profilesCommand()

  • case "config":
  •   return c.configCommand(args[1:])
    
  • case "discover":
  •   return c.discoverCommand(args[1:])
    
    case "init":
    return c.initCommand(args[1:])
    case "doctor":
    @@ -79,7 +83,9 @@ Usage:
    wikiforge init [--config wikiforge.yaml] [--force]
    wikiforge doctor [--config wikiforge.yaml]
    wikiforge profiles
  • wikiforge plan [--config wikiforge.yaml] [--component ID] [--skip-system]
  • wikiforge config migrate [--config wikiforge.yaml] [--output wikiforge.v3.json] [--force]
  • wikiforge discover [--config wikiforge.yaml] [--component ID]
  • wikiforge plan [--config wikiforge.yaml] [--component ID] [--skip-system] [--explain]
    wikiforge generate [--config wikiforge.yaml] [--component ID] [--skip-system] [--resume]
    wikiforge update [--config wikiforge.yaml] [--component ID] [--skip-system]
    wikiforge resume [--config wikiforge.yaml]
    @@ -124,6 +130,99 @@ func (c CLI) profilesCommand() int {
    profile, _ := prompts.GetProfile(profileID)
    fmt.Fprintf(c.Out, " %-20s pages=%-2d phases=%-2d %s\n", profile.ID, len(prompts.ExpectedFiles(profile)), len(profile.Phases), profile.Description)
    }
  • fmt.Fprintln(c.Out, "\nComposable capability packs:")
  • for _, pack := range config.SupportedCapabilityPacks() {
  •   fmt.Fprintln(c.Out, "  "+pack)
    
  • }
  • fmt.Fprintln(c.Out, "\nAdaptive documentation views:")
  • for _, view := range config.SupportedViews() {
  •   fmt.Fprintln(c.Out, "  "+view)
    
  • }
  • return 0
    +}

+func (c CLI) configCommand(args []string) int {

  • if len(args) == 0 || args[0] != "migrate" {
  •   fmt.Fprintln(c.Err, "usage: wikiforge config migrate [--config PATH] [--output PATH] [--force]")
    
  •   return 2
    
  • }
  • fs, cfgPath := commonFlags("config migrate")
  • output := fs.String("output", "wikiforge.v3.json", "normalized version 3 output")
  • force := fs.Bool("force", false, "overwrite output")
  • if err := fs.Parse(args[1:]); err != nil {
  •   return c.flagError(err)
    
  • }
  • if _, err := os.Stat(*output); err == nil && !*force {
  •   return c.printErr(fmt.Errorf("%s already exists; use --force to replace it", *output))
    
  • }
  • cfg, err := config.Load(*cfgPath)
  • if err != nil {
  •   return c.printErr(err)
    
  • }
  • data, err := cfg.NormalizedJSONRelativeTo(filepath.Dir(*output))
  • if err != nil {
  •   return c.printErr(err)
    
  • }
  • data = append(data, '\n')
  • if err := os.MkdirAll(filepath.Dir(*output), 0o755); err != nil {
  •   return c.printErr(err)
    
  • }
  • tmp := *output + ".tmp"
  • if err := os.WriteFile(tmp, data, 0o644); err != nil {
  •   return c.printErr(err)
    
  • }
  • if err := replaceFile(tmp, *output, *force); err != nil {
  •   return c.printErr(err)
    
  • }
  • fmt.Fprintf(c.Out, "migrated source version %d to version %d: %s\n", cfg.SourceVersion, config.CurrentVersion, *output)
  • return 0
    +}

+func replaceFile(tmp, destination string, replace bool) error {

  • if !replace {
  •   if err := os.Rename(tmp, destination); err != nil {
    
  •   	_ = os.Remove(tmp)
    
  •   	return err
    
  •   }
    
  •   return nil
    
  • }
  • if err := os.Rename(tmp, destination); err == nil {
  •   return nil
    
  • }

Copy link
Copy Markdown
Owner Author

WIKIFORGE_PHASE1_PATCH_PART 04/19

  • if err := os.Remove(destination); err != nil && !os.IsNotExist(err) {
  •   _ = os.Remove(tmp)
    
  •   return err
    
  • }
  • if err := os.Rename(tmp, destination); err != nil {
  •   _ = os.Remove(tmp)
    
  •   return err
    
  • }
  • return nil
    +}

+func (c CLI) discoverCommand(args []string) int {

  • fs, cfgPath := commonFlags("discover")
  • component, legacy := componentFlag(fs)
  • if err := fs.Parse(args); err != nil {
  •   return c.flagError(err)
    
  • }
  • selected, err := resolveComponentFlag(*component, *legacy)
  • if err != nil {
  •   return c.printErr(err)
    
  • }
  • cfg, err := config.Load(*cfgPath)
  • if err != nil {
  •   return c.printErr(err)
    
  • }
  • o := orchestrator.New(cfg, openwiki.ExecRunner{Config: cfg.OpenWiki, Out: c.Out}, c.Out)
  • manifests, err := o.Discover(selected, true)
  • if err != nil {
  •   return c.printErr(err)
    
  • }
  • for _, manifest := range manifests {
  •   data, _ := json.MarshalIndent(manifest, "", "  ")
    
  •   fmt.Fprintf(c.Out, "[%s]\n%s\n", manifest.Component.ID, data)
    
  • }
    return 0
    }

@@ -187,28 +286,7 @@ func (c CLI) doctorCommand(ctx context.Context, args []string) int {
}
}
for _, component := range cfg.EnabledComponents() {

  •   statErr := func() error {
    
  •   	if _, e := os.Stat(component.Repository); e != nil {
    
  •   		return fmt.Errorf("repository: %w", e)
    
  •   	}
    
  •   	if !isGitRepo(component.Repository) {
    
  •   		return errors.New("repository is not a Git work tree")
    
  •   	}
    
  •   	if _, e := os.Stat(component.WorkDir()); e != nil {
    
  •   		return fmt.Errorf("scope %q: %w", component.Scope, e)
    
  •   	}
    
  •   	if e := checkExternalPath(component.Repository); e != nil {
    
  •   		return fmt.Errorf("repository path portability: %w", e)
    
  •   	}
    
  •   	if e := checkExternalPath(component.WorkDir()); e != nil {
    
  •   		return fmt.Errorf("scope path portability: %w", e)
    
  •   	}
    
  •   	if e := openwiki.CheckPromptTransport(component.WorkDir()); e != nil {
    
  •   		return fmt.Errorf("prompt-file transport: %w", e)
    
  •   	}
    
  •   	return nil
    
  •   }()
    
  •   check(fmt.Sprintf("component %s (%s/%s)", component.ID, component.Type, component.Profile), statErr)
    
  •   check(fmt.Sprintf("component %s (%s/%s)", component.ID, component.Type, component.Profile), checkComponentEnvironment(component, cfg.UnitsForComponent(component.ID)))
    
    }
    if cfg.System.Enabled && cfg.System.FactsPath != "" {
    if _, err := os.Stat(cfg.System.FactsPath); err != nil && !errors.Is(err, os.ErrNotExist) {
    @@ -221,10 +299,40 @@ func (c CLI) doctorCommand(ctx context.Context, args []string) int {
    return 0
    }

+func checkComponentEnvironment(component config.ComponentConfig, units []config.DocumentationUnitConfig) error {

  • if _, err := os.Stat(component.Repository); err != nil {
  •   return fmt.Errorf("repository: %w", err)
    
  • }
  • if !isGitRepo(component.Repository) {
  •   return errors.New("repository is not a Git work tree")
    
  • }
  • if _, err := os.Stat(component.WorkDir()); err != nil {
  •   return fmt.Errorf("scope %q: %w", component.Scope, err)
    
  • }
  • if err := checkExternalPath(component.Repository); err != nil {
  •   return fmt.Errorf("repository path portability: %w", err)
    
  • }
  • if err := checkExternalPath(component.WorkDir()); err != nil {
  •   return fmt.Errorf("scope path portability: %w", err)
    
  • }
  • for _, unit := range units {
  •   for _, root := range unit.SourceRoots {
    
  •   	if _, err := os.Stat(filepath.Join(component.WorkDir(), filepath.FromSlash(root))); err != nil {
    
  •   		return fmt.Errorf("documentation unit %s source root %q: %w", unit.ID, root, err)
    
  •   	}
    
  •   }
    
  • }
  • if err := openwiki.CheckPromptTransport(component.WorkDir()); err != nil {
  •   return fmt.Errorf("prompt-file transport: %w", err)
    
  • }
  • return nil
    +}

func (c CLI) planCommand(args []string) int {
fs, cfgPath := commonFlags("plan")
component, legacy := componentFlag(fs)
skip := fs.Bool("skip-system", false, "skip whole-system plan")

  • explain := fs.Bool("explain", false, "include skipped and deferred planning decisions")
    if err := fs.Parse(args); err != nil {
    return c.flagError(err)
    }
    @@ -237,7 +345,11 @@ func (c CLI) planCommand(args []string) int {
    return c.printErr(err)
    }
    o := orchestrator.New(cfg, openwiki.ExecRunner{Config: cfg.OpenWiki, Out: c.Out, LiveOutput: true}, c.Out)
  • for _, line := range o.Plan(selected, !*skip) {
  • lines, err := o.PlanWithExplain(selected, !*skip, *explain)
  • if err != nil {
  •   return c.printErr(err)
    
  • }
  • for _, line := range lines {
    fmt.Fprintln(c.Out, line)
    }
    return 0
    diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go
    new file mode 100644
    index 0000000000000000000000000000000000000000..c9926cbce4af45a9a950b4bbd1c4306e7a88fb36
    --- /dev/null
    +++ b/internal/cli/cli_test.go
    @@ -0,0 +1,179 @@
    +package cli

+import (

  • "bytes"
  • "context"
  • "encoding/json"
  • "os"
  • "os/exec"
  • "path/filepath"
  • "strings"
  • "testing"
  • "github.com/example/wikiforge/internal/config"
    +)

+func TestDiscoverAndExplainPlanPersistAdaptiveArtifacts(t *testing.T) {

  • root := t.TempDir()
  • repo := filepath.Join(root, "app")
  • if err := os.MkdirAll(filepath.Join(repo, "modules", "orders"), 0o755); err != nil {
  •   t.Fatal(err)
    
  • }
  • if err := os.WriteFile(filepath.Join(repo, "modules", "orders", "api.go"), []byte("http handler kafka producer redis cache"), 0o644); err != nil {
  •   t.Fatal(err)
    
  • }
  • cfgPath := filepath.Join(root, "wikiforge.yaml")
  • writeConfig(t, cfgPath, `version: 3
    +workspace: .
    +openwiki:
  • command: npx
    +mermaid:
  • mode: basic
    +components:
    • id: app
  • type: modular-monolith
  • repository: ./app
  • enabled: true
  • packs: [workflow]
    +documentationUnits:
    • id: orders
  • component: app
  • kind: domain
  • sourceRoots: [modules/orders]
    +system:
  • enabled: false
  • output: ./system
    +`)
  • var out, errOut bytes.Buffer
  • command := CLI{Out: &out, Err: &errOut}
  • if code := command.Run(context.Background(), []string{"discover", "--config", cfgPath, "--component", "app"}); code != 0 {
  •   t.Fatalf("discover code=%d err=%s", code, errOut.String())
    
  • }
  • for _, name := range []string{"discovery.json", "plan.json"} {
  •   path := filepath.Join(root, ".wikiforge", "components", "app", name)
    
  •   data, err := os.ReadFile(path)
    
  •   if err != nil {
    
  •   	t.Fatal(err)
    
  •   }
    
  •   var value map[string]any
    
  •   if err := json.Unmarshal(data, &value); err != nil {
    
  •   	t.Fatalf("%s: %v", name, err)
    
  •   }
    
  • }
  • out.Reset()
  • errOut.Reset()
  • if code := command.Run(context.Background(), []string{"plan", "--config", cfgPath, "--component", "app", "--skip-system", "--explain"}); code != 0 {
  •   t.Fatalf("plan code=%d err=%s", code, errOut.String())
    
  • }
  • text := out.String()
  • for _, wanted := range []string{"adaptive plan component=app", "catalogs/interfaces/index.md", "orders", "skip"} {
  •   if !strings.Contains(text, wanted) {
    
  •   	t.Errorf("plan missing %q:\n%s", wanted, text)
    
  •   }
    
  • }
    +}

+func TestConfigMigrateWritesNormalizedV3(t *testing.T) {

  • root := t.TempDir()
  • cfgPath := filepath.Join(root, "v2.yaml")
  • output := filepath.Join(root, "v3.json")
  • writeConfig(t, cfgPath, `version: 2
    +openwiki:
  • command: npx
    +mermaid:
  • mode: basic
    +components:
    • id: app
  • type: microservice
  • repository: ./app
  • enabled: true
    +system:
  • enabled: false
  • output: ./system
    +`)
  • var out, errOut bytes.Buffer
  • command := CLI{Out: &out, Err: &errOut}
  • if code := command.Run(context.Background(), []string{"config", "migrate", "--config", cfgPath, "--output", output}); code != 0 {
  •   t.Fatalf("code=%d err=%s", code, errOut.String())
    
  • }
  • data, err := os.ReadFile(output)
  • if err != nil {
  •   t.Fatal(err)
    
  • }
  • var migrated struct {
  •   Version            int   `json:"version"`
    
  •   DocumentationUnits []any `json:"documentationUnits"`
    
  • }
  • if err := json.Unmarshal(data, &migrated); err != nil {
  •   t.Fatal(err)
    
  • }
  • if migrated.Version != 3 {
  •   t.Fatalf("version=%d", migrated.Version)
    
  • }
  • if strings.Contains(string(data), filepath.ToSlash(root)+"/") {
  •   t.Fatalf("migration unexpectedly embedded absolute workspace paths: %s", data)
    
  • }
  • if !strings.Contains(out.String(), "source version 2 to version 3") {
  •   t.Fatalf("output=%s", out.String())
    
  • }
  • if err := os.MkdirAll(filepath.Join(root, "app"), 0o755); err != nil {
  •   t.Fatal(err)
    
  • }
  • if err := os.WriteFile(filepath.Join(root, "app", "README.md"), []byte("# app\n"), 0o644); err != nil {
  •   t.Fatal(err)
    
  • }
  • var secondOut, secondErr bytes.Buffer
  • if code := (CLI{Out: &secondOut, Err: &secondErr}).Run(context.Background(), []string{"plan", "--config", output, "--component", "app", "--skip-system"}); code != 0 {
  •   t.Fatalf("migrated config is not reloadable: code=%d err=%s", code, secondErr.String())
    
  • }
    +}

+func writeConfig(t *testing.T, path, value string) {

  • t.Helper()
  • if err := os.WriteFile(path, []byte(value), 0o644); err != nil {
  •   t.Fatal(err)
    
  • }
    +}

+func TestPlanReturnsFailureForUnknownComponent(t *testing.T) {

  • root := t.TempDir()
  • repo := filepath.Join(root, "app")
  • if err := os.MkdirAll(repo, 0o755); err != nil {
  •   t.Fatal(err)
    
  • }
  • cfgPath := filepath.Join(root, "wikiforge.yaml")
  • writeConfig(t, cfgPath, `version: 3
    +openwiki:
  • command: npx
    +mermaid:
  • mode: basic
    +components:
    • id: app
  • type: microservice
  • repository: ./app
  • enabled: true
    +system:
  • enabled: false
  • output: ./system
    +`)
  • var out, errOut bytes.Buffer
  • code := (CLI{Out: &out, Err: &errOut}).Run(context.Background(), []string{"plan", "--config", cfgPath, "--component", "missing", "--skip-system"})
  • if code == 0 || !strings.Contains(errOut.String(), "no enabled component matched") {
  •   t.Fatalf("code=%d out=%s err=%s", code, out.String(), errOut.String())
    
  • }
    +}

+func TestCheckComponentEnvironmentRejectsMissingDocumentationUnitRoot(t *testing.T) {

  • repo := t.TempDir()
  • cmd := exec.Command("git", "init")
  • cmd.Dir = repo
  • if output, err := cmd.CombinedOutput(); err != nil {
  •   t.Fatalf("git init: %v: %s", err, output)
    
  • }
  • component := config.ComponentConfig{ID: "app", Type: "microservice", Profile: "application", Repository: repo, Enabled: true}
  • units := []config.DocumentationUnitConfig{{ID: "orders", Component: "app", Kind: "domain", SourceRoots: []string{"modules/orders"}}}
  • err := checkComponentEnvironment(component, units)
  • if err == nil || !strings.Contains(err.Error(), "documentation unit orders source root") {
  •   t.Fatalf("unexpected error: %v", err)
    
  • }
    +}
    diff --git a/internal/config/config.go b/internal/config/config.go
    index a9b077a609a860a3f50e4060b4d1ef5dec1abff9..93b5a3cc5329013b6024ec559b66cbdbddd91ad2 100644
    --- a/internal/config/config.go
    +++ b/internal/config/config.go
    @@ -12,20 +12,61 @@ import (
    "github.com/example/wikiforge/internal/pathutil"
    )

-const CurrentVersion = 2
+const CurrentVersion = 3
+
+var defaultViews = []string{"system", "domain", "component", "flow", "catalog", "platform", "engineering", "operations"}
+
+var knownViews = map[string]bool{

  • "system": true, "domain": true, "component": true, "flow": true,
  • "catalog": true, "platform": true, "engineering": true, "operations": true,
    +}

+var knownUnitKinds = map[string]bool{

  • "domain": true, "subdomain": true, "bounded-context": true, "component": true,
  • "module": true, "flow": true, "platform": true, "catalog": true,
    +}

+var shardDimensions = []string{"bounded-context", "component", "criticality", "data-store", "domain", "owner", "repository", "runtime", "subdomain", "transport"}
+
+var knownShardDimensions = map[string]bool{

  • "domain": true, "subdomain": true, "bounded-context": true, "component": true,
  • "owner": true, "repository": true, "runtime": true, "transport": true,
  • "data-store": true, "criticality": true,
    +}

+var criticalities = []string{"critical", "high", "low", "medium"}
+var knownCriticalities = map[string]bool{"": true, "low": true, "medium": true, "high": true, "critical": true}
+
+var capabilityPacks = []string{

  • "api", "cache", "concurrency", "configuration", "container-runtime", "cryptography",
  • "data-access", "database", "distributed-coordination", "domain", "engineering",

Copy link
Copy Markdown
Owner Author

WIKIFORGE_PHASE1_PATCH_PART 05/19

  • "files", "jobs", "messaging", "migrations", "rate-limit", "runtime", "security",
  • "telemetry", "workflow",
    +}

+var defaultPacksByProfile = map[string][]string{

  • "application": {"api", "configuration", "domain", "engineering", "runtime", "security", "telemetry"},
  • "modular-application": {"api", "configuration", "domain", "engineering", "runtime", "security", "telemetry"},
  • "reusable": {"api", "configuration", "concurrency", "engineering", "security"},
  • "infrastructure": {"configuration", "container-runtime", "engineering", "security", "telemetry"},
  • "configuration": {"configuration", "engineering", "security"},
  • "contracts": {"api", "domain", "engineering"},
  • "generic": {"configuration", "engineering"},
    +}

type Config struct {

  • Version int json:"version"
  • Workspace string json:"workspace"
  • OpenWiki OpenWikiConfig json:"openwiki"
  • Execution ExecutionConfig json:"execution"
  • Documentation DocumentationConfig json:"documentation"
  • Mermaid MermaidConfig json:"mermaid"
  • Components []ComponentConfig json:"components"
  • Version int json:"version"
  • Workspace string json:"workspace"
  • OpenWiki OpenWikiConfig json:"openwiki"
  • Execution ExecutionConfig json:"execution"
  • Documentation DocumentationConfig json:"documentation"
  • Mermaid MermaidConfig json:"mermaid"
  • Components []ComponentConfig json:"components"
  • DocumentationUnits []DocumentationUnitConfig json:"documentationUnits,omitempty"
    // Services is retained only for backward compatibility with v1 configurations.
  • // Each legacy service is normalized into a component of type microservice.
  • Services []ServiceConfig json:"services"
  • System SystemConfig json:"system"
  • Services []ServiceConfig json:"services,omitempty"
  • System SystemConfig json:"system"
  • SourceVersion int json:"-"
    }

type OpenWikiConfig struct {
@@ -38,23 +79,37 @@ type OpenWikiConfig struct {

type ExecutionConfig struct {
ParallelComponents int json:"parallelComponents"

  • ParallelServices int json:"parallelServices" // legacy alias
  • ParallelServices int json:"parallelServices,omitempty" // legacy alias
    MaxProcessRetries int json:"maxProcessRetries"
    MaxRepairRounds int json:"maxRepairRounds"
    ContinueOnComponentFailure bool json:"continueOnComponentFailure"
  • ContinueOnServiceFailure bool json:"continueOnServiceFailure" // legacy alias
  • ContinueOnServiceFailure bool json:"continueOnServiceFailure,omitempty" // legacy alias
    }

type DocumentationConfig struct {

  • Language string json:"language"
  • MinimumQualityScore int json:"minimumQualityScore"
  • MinimumPages int json:"minimumPages"
  • RequireFrontMatter bool json:"requireFrontMatter"
  • RequireSourceReferences bool json:"requireSourceReferences"
  • ValidateSourcePaths bool json:"validateSourcePaths"
  • RequireMermaid bool json:"requireMermaid"
  • MinimumMermaidBlocks int json:"minimumMermaidBlocks"
  • AllowedDiagramTypes []string json:"allowedDiagramTypes"
  • Language string json:"language"
  • MinimumQualityScore int json:"minimumQualityScore"
  • MinimumPages int json:"minimumPages"
  • RequireFrontMatter bool json:"requireFrontMatter"
  • RequireSourceReferences bool json:"requireSourceReferences"
  • ValidateSourcePaths bool json:"validateSourcePaths"
  • RequireMermaid bool json:"requireMermaid"
  • MinimumMermaidBlocks int json:"minimumMermaidBlocks"
  • AllowedDiagramTypes []string json:"allowedDiagramTypes"
  • Views []string json:"views,omitempty"
  • Catalogs CatalogConfig json:"catalogs,omitempty"
  • Evidence EvidenceConfig json:"evidence,omitempty"
    +}

+type CatalogConfig struct {

  • ShardBy []string json:"shardBy,omitempty"
  • MaximumRowsPerPage int json:"maximumRowsPerPage,omitempty"
    +}

+type EvidenceConfig struct {

  • Include []string json:"include,omitempty"
  • Exclude []string json:"exclude,omitempty"
  • MaxFileSizeBytes int64 json:"maxFileSizeBytes,omitempty"
    }

type MermaidConfig struct {
@@ -70,12 +125,27 @@ type ComponentConfig struct {
Profile string json:"profile"
Repository string json:"repository"
Scope string json:"scope"

  • Path string json:"path" // accepted alias for repository or scoped path
  • Path string json:"path,omitempty" // accepted legacy alias
    Enabled bool json:"enabled"
    IncludeInSystem *bool json:"includeInSystem"
    Group string json:"group"
    Tags []string json:"tags"
    DependsOn []string json:"dependsOn"
  • Owners []string json:"owners,omitempty"
  • Capabilities []string json:"capabilities,omitempty"
  • Packs []string json:"packs,omitempty"
    +}

+type DocumentationUnitConfig struct {

  • ID string json:"id"
  • Component string json:"component"
  • Kind string json:"kind"
  • SourceRoots []string json:"sourceRoots,omitempty"
  • RelatedUnits []string json:"relatedUnits,omitempty"
  • Output string json:"output,omitempty"
  • Owners []string json:"owners,omitempty"
  • Capabilities []string json:"capabilities,omitempty"
  • Criticality string json:"criticality,omitempty"
    }

// ServiceConfig is the legacy v1 shape.
@@ -96,8 +166,9 @@ type SystemConfig struct {

func Defaults() Config {
return Config{

  •   Version:   CurrentVersion,
    
  •   Workspace: ".",
    
  •   Version:       CurrentVersion,
    
  •   SourceVersion: CurrentVersion,
    
  •   Workspace:     ".",
      OpenWiki: OpenWikiConfig{
      	Command:        "npx",
      	Args:           []string{"--yes", "openwiki@0.2.0", "code"},
    

@@ -116,13 +187,20 @@ func Defaults() Config {
Documentation: DocumentationConfig{
Language: "English",
MinimumQualityScore: 85,

  •   	MinimumPages:            0, // zero means use the selected profile contract
    
  •   	MinimumPages:            0,
      	RequireFrontMatter:      true,
      	RequireSourceReferences: true,
      	ValidateSourcePaths:     true,
      	RequireMermaid:          true,
    
  •   	MinimumMermaidBlocks:    0, // zero means use the selected profile contract
    
  •   	MinimumMermaidBlocks:    0,
      	AllowedDiagramTypes:     []string{"flowchart", "sequenceDiagram", "stateDiagram-v2", "erDiagram", "classDiagram", "architecture-beta", "gitGraph", "mindmap"},
    
  •   	Views:                   append([]string(nil), defaultViews...),
    
  •   	Catalogs:                CatalogConfig{ShardBy: []string{"domain", "owner"}, MaximumRowsPerPage: 150},
    
  •   	Evidence: EvidenceConfig{
    
  •   		Include:          []string{"**"},
    
  •   		Exclude:          []string{".git/**", ".wikiforge/**", "openwiki/**", "vendor/**", "node_modules/**", "dist/**", "build/**", "target/**", "generated/**", ".wikiforge-prompt-*.md", "**/*.bin"},
    
  •   		MaxFileSizeBytes: 2 * 1024 * 1024,
    
  •   	},
      },
      Mermaid: MermaidConfig{
      	Mode:           "render",
    

@@ -169,12 +247,19 @@ func Load(path string) (Config, error) {
if err != nil {
return cfg, fmt.Errorf("workspace path: %w", err)
}

  • normalizeLegacy(&cfg)
  • if err := normalizeLegacy(&cfg); err != nil {
  •   return cfg, err
    
  • }
    for i := range cfg.Components {
    if err := normalizeComponent(base, &cfg.Components[i]); err != nil {
    return cfg, fmt.Errorf("component %q paths: %w", cfg.Components[i].ID, err)
    }
    }
  • for i := range cfg.DocumentationUnits {
  •   if err := normalizeDocumentationUnit(&cfg.DocumentationUnits[i]); err != nil {
    
  •   	return cfg, fmt.Errorf("documentation unit %q: %w", cfg.DocumentationUnits[i].ID, err)
    
  •   }
    
  • }
    cfg.System.Output, err = pathutil.Resolve(base, cfg.System.Output)
    if err != nil {
    return cfg, fmt.Errorf("system.output path: %w", err)
    @@ -189,18 +274,20 @@ func Load(path string) (Config, error) {
    return cfg, Validate(cfg)
    }

-func normalizeLegacy(c *Config) {
+func normalizeLegacy(c *Config) error {
if c.Version == 0 {
c.Version = 1
}

  • if c.Version < 1 || c.Version > CurrentVersion {
  •   return fmt.Errorf("unsupported config version %d; supported versions are 1, 2, and %d", c.Version, CurrentVersion)
    
  • }
  • c.SourceVersion = c.Version
    for _, s := range c.Services {
  •   c.Components = append(c.Components, ComponentConfig{
    
  •   	ID:         s.ID,
    
  •   	Type:       "microservice",
    
  •   	Repository: s.Path,
    
  •   	Enabled:    s.Enabled,
    
  •   })
    
  •   c.Components = append(c.Components, ComponentConfig{ID: s.ID, Type: "microservice", Repository: s.Path, Enabled: s.Enabled})
    
    }
  • c.Services = nil
  • c.Version = CurrentVersion
  • return nil
    }

func normalizeComponent(base string, c *ComponentConfig) error {
@@ -219,7 +306,7 @@ func normalizeComponent(base string, c *ComponentConfig) error {
if err != nil {
return fmt.Errorf("repository: %w", err)
}

  • c.Path = "" // prevent ambiguous use after normalization
  • c.Path = ""
    if c.Type == "" {
    c.Type = "generic"
    }
    @@ -230,15 +317,52 @@ func normalizeComponent(base string, c *ComponentConfig) error {
    v := true
    c.IncludeInSystem = &v
    }
  • sort.Strings(c.Tags)
  • sort.Strings(c.DependsOn)
  • c.Tags = sortedUnique(c.Tags)
  • c.DependsOn = sortedUnique(c.DependsOn)
  • c.Owners = sortedUnique(c.Owners)
  • c.Capabilities = sortedUnique(c.Capabilities)
  • for i := range c.Packs {
  •   c.Packs[i] = normalizeID(c.Packs[i])
    
  • }
  • c.Packs = sortedUnique(c.Packs)
  • return nil
    +}

+func normalizeDocumentationUnit(u *DocumentationUnitConfig) error {

  • u.ID = strings.TrimSpace(u.ID)
  • u.Component = strings.TrimSpace(u.Component)
  • u.Kind = normalizeID(u.Kind)
  • u.Output = strings.TrimSpace(u.Output)
  • var err error
  • if u.Output != "" {
  •   u.Output, err = pathutil.NormalizeRelative(u.Output)
    
  •   if err != nil {
    
  •   	return fmt.Errorf("output: %w", err)
    
  •   }
    
  •   // Documentation paths are serialized and consumed as bundle-relative paths,
    
  •   // so keep them canonical across Windows, macOS, and Linux.
    
  •   u.Output = filepath.ToSlash(u.Output)
    
  • }
  • for i, root := range u.SourceRoots {
  •   u.SourceRoots[i], err = pathutil.NormalizeRelative(root)
    
  •   if err != nil {
    
  •   	return fmt.Errorf("sourceRoots[%d]: %w", i, err)
    
  •   }
    
  •   u.SourceRoots[i] = filepath.ToSlash(u.SourceRoots[i])
    
  • }
  • u.SourceRoots = sortedUnique(u.SourceRoots)
  • u.RelatedUnits = sortedUnique(u.RelatedUnits)
  • u.Owners = sortedUnique(u.Owners)
  • u.Capabilities = sortedUnique(u.Capabilities)
  • u.Criticality = normalizeID(u.Criticality)
    return nil
    }

func applyDefaults(c *Config) {
d := Defaults()

  • if c.Version == 0 {
  •   c.Version = CurrentVersion
    
  • c.Version = CurrentVersion
  • if c.SourceVersion == 0 {
  •   c.SourceVersion = CurrentVersion
    
    }
    if c.OpenWiki.Command == "" {
    c.OpenWiki.Command = d.OpenWiki.Command
    @@ -277,6 +401,29 @@ func applyDefaults(c *Config) {
    if len(c.Documentation.AllowedDiagramTypes) == 0 {
    c.Documentation.AllowedDiagramTypes = d.Documentation.AllowedDiagramTypes
    }
  • if c.Documentation.Views == nil {
  •   c.Documentation.Views = append([]string(nil), d.Documentation.Views...)
    
  • }
  • for i := range c.Documentation.Views {
  •   c.Documentation.Views[i] = normalizeID(c.Documentation.Views[i])
    
  • }
  • c.Documentation.Views = sortedUnique(c.Documentation.Views)
  • if c.Documentation.Catalogs.ShardBy == nil {
  •   c.Documentation.Catalogs.ShardBy = append([]string(nil), d.Documentation.Catalogs.ShardBy...)
    
  • }
  • c.Documentation.Catalogs.ShardBy = sortedUnique(c.Documentation.Catalogs.ShardBy)
    +WIKIFORGE_PHASE1_PATCH_PART 06/19
  • if c.Documentation.Catalogs.MaximumRowsPerPage <= 0 {
  •   c.Documentation.Catalogs.MaximumRowsPerPage = d.Documentation.Catalogs.MaximumRowsPerPage
    
  • }
  • if c.Documentation.Evidence.Include == nil {
  •   c.Documentation.Evidence.Include = append([]string(nil), d.Documentation.Evidence.Include...)
    
  • }
  • if c.Documentation.Evidence.Exclude == nil {
  •   c.Documentation.Evidence.Exclude = append([]string(nil), d.Documentation.Evidence.Exclude...)
    
  • }
  • if c.Documentation.Evidence.MaxFileSizeBytes <= 0 {
  •   c.Documentation.Evidence.MaxFileSizeBytes = d.Documentation.Evidence.MaxFileSizeBytes
    
  • }
    if c.Mermaid.Mode == "" {
    c.Mermaid.Mode = d.Mermaid.Mode
    }
    @@ -298,13 +445,14 @@ func applyDefaults(c *Config) {
    }

func Validate(c Config) error {

  • if c.Version != 1 && c.Version != CurrentVersion {
  •   return fmt.Errorf("unsupported config version %d; supported versions are 1 and %d", c.Version, CurrentVersion)
    
  • if c.Version != CurrentVersion {
  •   return fmt.Errorf("configuration was not normalized to version %d", CurrentVersion)
    
    }
    if c.OpenWiki.Command == "" {
    return errors.New("openwiki.command is required")
    }
    allIDs := map[string]bool{}
  • portableComponentIDs := map[string]string{}
    enabledIDs := map[string]bool{}
    workdirs := map[string]string{}
    for _, component := range c.Components {
    @@ -320,7 +468,17 @@ func Validate(c Config) error {
    if allIDs[component.ID] {
    return fmt.Errorf("duplicate component id %q", component.ID)
    }
  •   portableID := strings.ToLower(component.ID)
    
  •   if other, exists := portableComponentIDs[portableID]; exists {
    
  •   	return fmt.Errorf("component ids %q and %q differ only by case and are not portable", other, component.ID)
    
  •   }
    
  •   portableComponentIDs[portableID] = component.ID
      allIDs[component.ID] = true
    
  •   for _, pack := range component.Packs {
    
  •   	if !KnownCapabilityPack(pack) {
    
  •   		return fmt.Errorf("component %q has unsupported capability pack %q", component.ID, pack)
    
  •   	}
    
  •   }
      if !component.Enabled {
      	continue
      }
    

@@ -335,10 +493,11 @@ func Validate(c Config) error {
return fmt.Errorf("component %q scope: %w", component.ID, err)
}
workdir := filepath.Clean(component.WorkDir())

  •   if other, ok := workdirs[workdir]; ok {
    
  •   	return fmt.Errorf("components %q and %q resolve to the same work directory %q", other, component.ID, workdir)
    
  •   workdirKey := strings.ToLower(filepath.ToSlash(workdir))
    
  •   if other, ok := workdirs[workdirKey]; ok {
    
  •   	return fmt.Errorf("components %q and %q resolve to the same portable work directory %q", other, component.ID, workdir)
      }
    
  •   workdirs[workdir] = component.ID
    
  •   workdirs[workdirKey] = component.ID
    
    }
    if len(enabledIDs) == 0 {
    return errors.New("at least one enabled component is required")
    @@ -350,6 +509,78 @@ func Validate(c Config) error {
    }
    }
    }
  • unitIDs := map[string]bool{}
  • portableUnitIDs := map[string]string{}
  • unitsByID := map[string][]string{}
  • outputs := map[string]string{}
  • for _, unit := range c.DocumentationUnits {
  •   if err := pathutil.ValidatePortableSegment(unit.ID); err != nil {
    
  •   	return fmt.Errorf("documentation unit id %q is not portable: %w", unit.ID, err)
    
  •   }
    
  •   key := unit.Component + "/" + unit.ID
    
  •   if unitIDs[key] {
    
  •   	return fmt.Errorf("duplicate documentation unit %q in component %q", unit.ID, unit.Component)
    
  •   }
    
  •   portableKey := strings.ToLower(key)
    
  •   if other, exists := portableUnitIDs[portableKey]; exists {
    
  •   	return fmt.Errorf("documentation units %q and %q differ only by case and are not portable", other, key)
    
  •   }
    
  •   portableUnitIDs[portableKey] = key
    
  •   unitIDs[key] = true
    
  •   unitsByID[unit.ID] = append(unitsByID[unit.ID], key)
    
  •   if !enabledIDs[unit.Component] {
    
  •   	return fmt.Errorf("documentation unit %q references unknown or disabled component %q", unit.ID, unit.Component)
    
  •   }
    
  •   if !knownUnitKinds[unit.Kind] {
    
  •   	return fmt.Errorf("documentation unit %q has unsupported kind %q", unit.ID, unit.Kind)
    
  •   }
    
  •   if !knownCriticalities[unit.Criticality] {
    
  •   	return fmt.Errorf("documentation unit %q has unsupported criticality %q", unit.ID, unit.Criticality)
    
  •   }
    
  •   if unit.Output != "" {
    
  •   	outputKey := strings.ToLower(unit.Component + ":" + unit.Output)
    
  •   	if other, ok := outputs[outputKey]; ok {
    
  •   		return fmt.Errorf("documentation units %q and %q share output %q", other, unit.ID, unit.Output)
    
  •   	}
    
  •   	outputs[outputKey] = unit.ID
    
  •   }
    
  • }
  • for _, unit := range c.DocumentationUnits {
  •   for _, related := range unit.RelatedUnits {
    
  •   	if strings.Contains(related, "/") {
    
  •   		if !unitIDs[related] {
    
  •   			return fmt.Errorf("documentation unit %q relates to unknown qualified unit %q", unit.ID, related)
    
  •   		}
    
  •   		continue
    
  •   	}
    
  •   	if unitIDs[unit.Component+"/"+related] {
    
  •   		continue
    
  •   	}
    
  •   	matches := unitsByID[related]
    
  •   	if len(matches) == 0 {
    
  •   		return fmt.Errorf("documentation unit %q relates to unknown unit %q", unit.ID, related)
    
  •   	}
    
  •   	if len(matches) > 1 {
    
  •   		return fmt.Errorf("documentation unit %q has ambiguous relation %q; use component/unit", unit.ID, related)
    
  •   	}
    
  •   }
    
  • }
  • for _, view := range c.Documentation.Views {
  •   if !knownViews[view] {
    
  •   	return fmt.Errorf("unsupported documentation view %q", view)
    
  •   }
    
  • }
  • for _, dimension := range c.Documentation.Catalogs.ShardBy {
  •   if !knownShardDimensions[dimension] {
    
  •   	return fmt.Errorf("unsupported catalog shard dimension %q", dimension)
    
  •   }
    
  • }
  • if c.Documentation.Catalogs.MaximumRowsPerPage < 1 {
  •   return errors.New("documentation.catalogs.maximumRowsPerPage must be positive")
    
  • }
  • if c.Documentation.Evidence.MaxFileSizeBytes < 1 {
  •   return errors.New("documentation.evidence.maxFileSizeBytes must be positive")
    
  • }
    if c.System.Enabled && c.System.Output == "" {
    return errors.New("system.output is required when system.enabled is true")
    }
    @@ -359,10 +590,7 @@ func Validate(c Config) error {
    return nil
    }

-func validateScope(scope string) error {

  • _, err := pathutil.NormalizeRelative(scope)
  • return err
    -}
    +func validateScope(scope string) error { _, err := pathutil.NormalizeRelative(scope); return err }

func (c ComponentConfig) WorkDir() string {
if c.Scope == "" {
@@ -370,11 +598,7 @@ func (c ComponentConfig) WorkDir() string {
}
return filepath.Clean(filepath.Join(c.Repository, c.Scope))
}

-func (c ComponentConfig) DocumentationRoot() string {

  • return filepath.Join(c.WorkDir(), "openwiki")
    -}

+func (c ComponentConfig) DocumentationRoot() string { return filepath.Join(c.WorkDir(), "openwiki") }
func (c ComponentConfig) IsIncludedInSystem() bool {
return c.IncludeInSystem == nil || *c.IncludeInSystem
}
@@ -390,65 +614,104 @@ func (c Config) EnabledComponents() []ComponentConfig {
return out
}

+func (c Config) UnitsForComponent(componentID string) []DocumentationUnitConfig {

  • var out []DocumentationUnitConfig
  • for _, unit := range c.DocumentationUnits {
  •   if unit.Component == componentID {
    
  •   	out = append(out, unit)
    
  •   }
    
  • }
  • sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID })
  • return out
    +}

+func (c Config) ViewEnabled(view string) bool {

  • view = normalizeID(view)
  • for _, candidate := range c.Documentation.Views {
  •   if candidate == view {
    
  •   	return true
    
  •   }
    
  • }
  • return false
    +}

+func (c Config) NormalizedJSON() ([]byte, error) { return c.NormalizedJSONRelativeTo("") }
+
+func (c Config) NormalizedJSONRelativeTo(base string) ([]byte, error) {

  • clone := c
  • clone.SourceVersion = 0
  • clone.Services = nil
  • clone.Execution.ParallelServices = 0
  • clone.Execution.ContinueOnServiceFailure = false
  • if base != "" {
  •   absBase, err := filepath.Abs(base)
    
  •   if err != nil {
    
  •   	return nil, err
    
  •   }
    
  •   rel := func(value string) string {
    
  •   	if value == "" {
    
  •   		return ""
    
  •   	}
    
  •   	candidate, err := filepath.Rel(absBase, value)
    
  •   	if err != nil {
    
  •   		return filepath.ToSlash(value)
    
  •   	}
    
  •   	if candidate == "" {
    
  •   		candidate = "."
    
  •   	}
    
  •   	return filepath.ToSlash(candidate)
    
  •   }
    
  •   clone.Workspace = rel(clone.Workspace)
    
  •   for i := range clone.Components {
    
  •   	clone.Components[i].Repository = rel(clone.Components[i].Repository)
    
  •   	clone.Components[i].Scope = filepath.ToSlash(clone.Components[i].Scope)
    
  •   }
    
  •   for i := range clone.DocumentationUnits {
    
  •   	clone.DocumentationUnits[i].Output = filepath.ToSlash(clone.DocumentationUnits[i].Output)
    
  •   	for j := range clone.DocumentationUnits[i].SourceRoots {
    
  •   		clone.DocumentationUnits[i].SourceRoots[j] = filepath.ToSlash(clone.DocumentationUnits[i].SourceRoots[j])
    
  •   	}
    
  •   }
    
  •   clone.System.Output = rel(clone.System.Output)
    
  •   clone.System.FactsPath = rel(clone.System.FactsPath)
    
  • }
  • return json.MarshalIndent(clone, "", " ")
    +}

var typeToProfile = map[string]string{

  • "generic": "generic",
  • "repository": "generic",
  • "application": "application",
  • "monolith": "application",
  • "microservice": "application",
  • "service": "application",
  • "worker": "application",
  • "gateway": "application",
  • "frontend": "application",
  • "cli": "application",
  • "modular-monolith": "modular-application",
  • "library": "reusable",
  • "shared-library": "reusable",
  • "internal-library": "reusable",
  • "framework": "reusable",
  • "sdk": "reusable",
  • "iac": "infrastructure",
  • "infrastructure": "infrastructure",
  • "gitops": "infrastructure",
  • "platform": "infrastructure",
  • "deployment": "infrastructure",
  • "configuration": "configuration",
  • "shared-config": "configuration",
  • "config": "configuration",
  • "contract": "contracts",
  • "contracts": "contracts",
  • "schema": "contracts",
  • "schemas": "contracts",
    -}

-func normalizeType(value string) string {

  • "generic": "generic", "repository": "generic", "application": "application", "monolith": "application",
  • "microservice": "application", "service": "application", "worker": "application", "gateway": "application",
  • "frontend": "application", "cli": "application", "modular-monolith": "modular-application",
  • "library": "reusable", "shared-library": "reusable", "internal-library": "reusable", "framework": "reusable", "sdk": "reusable",
  • "iac": "infrastructure", "infrastructure": "infrastructure", "gitops": "infrastructure", "platform": "infrastructure", "deployment": "infrastructure",
  • "configuration": "configuration", "shared-config": "configuration", "config": "configuration",
  • "contract": "contracts", "contracts": "contracts", "schema": "contracts", "schemas": "contracts",
    +}

+func normalizeType(value string) string { return normalizeID(value) }
+func normalizeID(value string) string {
value = strings.TrimSpace(strings.ToLower(value))
value = strings.ReplaceAll(value, "_", "-")
value = strings.ReplaceAll(value, " ", "-")
return value
}

func ProfileForType(componentType string) string {
if p, ok := typeToProfile[normalizeType(componentType)]; ok {
return p
}
return "generic"
}

func KnownType(componentType string) bool {
_, ok := typeToProfile[normalizeType(componentType)]
return ok
}

func KnownProfile(profile string) bool {

  • switch strings.TrimSpace(strings.ToLower(profile)) {
  • switch normalizeID(profile) {
    case "application", "modular-application", "reusable", "infrastructure", "configuration", "contracts", "generic":
    return true
  • default:
  •   return false
    
    }
  • return false
    }

func SupportedTypes() []string {
out := make([]string, 0, len(typeToProfile))
for t := range typeToProfile {
@@ -457,3 +720,29 @@ func SupportedTypes() []string {
sort.Strings(out)
return out
}
+func SupportedCapabilityPacks() []string { return append([]string(nil), capabilityPacks...) }
+func KnownCapabilityPack(pack string) bool {

  • pack = normalizeID(pack)
  • i := sort.SearchStrings(capabilityPacks, pack)
  • return i < len(capabilityPacks) && capabilityPacks[i] == pack
    +}
    +func DefaultPacksForProfile(profile string) []string {

Copy link
Copy Markdown
Owner Author

WIKIFORGE_PHASE1_PATCH_PART 07/19

  • return append([]string(nil), defaultPacksByProfile[normalizeID(profile)]...)
    +}
    +func SupportedViews() []string { return append([]string(nil), defaultViews...) }
    +func SupportedShardDimensions() []string { return append([]string(nil), shardDimensions...) }
    +func SupportedCriticalities() []string { return append([]string(nil), criticalities...) }

+func sortedUnique(values []string) []string {

  • seen := map[string]bool{}
  • out := make([]string, 0, len(values))
  • for _, value := range values {
  •   value = strings.TrimSpace(value)
    
  •   if value != "" && !seen[value] {
    
  •   	seen[value] = true
    
  •   	out = append(out, value)
    
  •   }
    
  • }
  • sort.Strings(out)
  • return out
    +}
    diff --git a/internal/config/config_test.go b/internal/config/config_test.go
    index e75d903470ba9fcb0ba594776e3d7e48ba24956a..d714ba3632919d88cccab1756db41360840af9d7 100644
    --- a/internal/config/config_test.go
    +++ b/internal/config/config_test.go
    @@ -163,3 +163,275 @@ func TestRejectsComponentIDsUnsafeAsCrossPlatformPathSegments(t *testing.T) {
    }
    }
    }

+func TestLoadV3DocumentationUnitsPacksViewsAndEvidence(t *testing.T) {

  • dir := t.TempDir()
  • path := filepath.Join(dir, "wikiforge.yaml")
  • content := `version: 3
    +workspace: .
    +openwiki:
  • command: npx
    +mermaid:
  • mode: basic
    +documentation:
  • views: [component, domain, flow, catalog, platform]
  • catalogs:
  • shardBy: [domain, owner]
  • maximumRowsPerPage: 50
  • evidence:
  • include: [src/, workflows/]
  • exclude: [generated/**]
  • maxFileSizeBytes: 10000
    +components:
    • id: app
  • type: modular-monolith
  • repository: ./app
  • enabled: true
  • owners: [team-b, team-a]
  • capabilities: [pricing, order-management]
  • packs: [workflow, messaging]
    +documentationUnits:
    • id: order-management
  • component: app
  • kind: domain
  • sourceRoots: [modules/order, workflows/order]
  • output: domains/order-management
    • id: submit-order
  • component: app
  • kind: flow
  • sourceRoots: [workflows/order/submit.bpmn]
  • relatedUnits: [order-management]
    +system:
  • enabled: false
  • output: ./system
    +`
  • if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
  •   t.Fatal(err)
    
  • }
  • cfg, err := Load(path)
  • if err != nil {
  •   t.Fatal(err)
    
  • }
  • if cfg.Version != 3 || cfg.SourceVersion != 3 {
  •   t.Fatalf("versions=%d/%d", cfg.Version, cfg.SourceVersion)
    
  • }
  • if len(cfg.DocumentationUnits) != 2 || len(cfg.UnitsForComponent("app")) != 2 {
  •   t.Fatalf("units=%+v", cfg.DocumentationUnits)
    
  • }
  • if cfg.Documentation.Catalogs.MaximumRowsPerPage != 50 || cfg.Documentation.Evidence.MaxFileSizeBytes != 10000 {
  •   t.Fatalf("documentation config=%+v", cfg.Documentation)
    
  • }
  • if got := cfg.Components[0].Packs; len(got) != 2 || got[0] != "messaging" || got[1] != "workflow" {
  •   t.Fatalf("packs=%v", got)
    
  • }
  • if cfg.DocumentationUnits[0].SourceRoots[0] != "modules/order" {
  •   t.Fatalf("root normalization=%v", cfg.DocumentationUnits[0].SourceRoots)
    
  • }
    +}

+func TestV2LoadsThroughV3CompatibilityAdapter(t *testing.T) {

  • dir := t.TempDir()
  • path := filepath.Join(dir, "wikiforge.yaml")
  • content := `version: 2
    +openwiki:
  • command: npx
    +mermaid:
  • mode: basic
    +components:
    • id: app
  • type: microservice
  • repository: ./app
  • enabled: true
    +system:
  • enabled: false
  • output: ./system
    +`
  • if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
  •   t.Fatal(err)
    
  • }
  • cfg, err := Load(path)
  • if err != nil {
  •   t.Fatal(err)
    
  • }
  • if cfg.Version != 3 || cfg.SourceVersion != 2 {
  •   t.Fatalf("versions=%d/%d", cfg.Version, cfg.SourceVersion)
    
  • }
  • data, err := cfg.NormalizedJSON()
  • if err != nil {
  •   t.Fatal(err)
    
  • }
  • if string(data) == "" || cfg.Services != nil {
  •   t.Fatal("normalized migration is empty or retained legacy services")
    
  • }
    +}

+func TestRejectsInvalidCapabilityPackAndDocumentationUnitReferences(t *testing.T) {

  • cfg := Defaults()
  • cfg.Components = []ComponentConfig{{ID: "app", Type: "microservice", Profile: "application", Repository: t.TempDir(), Enabled: true, Packs: []string{"not-a-pack"}}}
  • if err := Validate(cfg); err == nil {
  •   t.Fatal("expected invalid pack")
    
  • }
  • cfg.Components[0].Packs = nil
  • cfg.DocumentationUnits = []DocumentationUnitConfig{{ID: "orders", Component: "missing", Kind: "domain"}}
  • if err := Validate(cfg); err == nil {
  •   t.Fatal("expected unknown component")
    
  • }
  • cfg.DocumentationUnits = []DocumentationUnitConfig{{ID: "orders", Component: "app", Kind: "domain", RelatedUnits: []string{"missing"}}}
  • if err := Validate(cfg); err == nil {
  •   t.Fatal("expected unknown related unit")
    
  • }
    +}

+func TestDocumentationUnitIDsAreComponentScopedAndCrossReferencesCanBeQualified(t *testing.T) {

  • cfg := Defaults()
  • cfg.Components = []ComponentConfig{
  •   {ID: "a", Type: "microservice", Profile: "application", Repository: filepath.Join(t.TempDir(), "a"), Enabled: true},
    
  •   {ID: "b", Type: "microservice", Profile: "application", Repository: filepath.Join(t.TempDir(), "b"), Enabled: true},
    
  • }
  • cfg.DocumentationUnits = []DocumentationUnitConfig{
  •   {ID: "orders", Component: "a", Kind: "domain", RelatedUnits: []string{"b/orders"}},
    
  •   {ID: "orders", Component: "b", Kind: "domain"},
    
  • }
  • if err := Validate(cfg); err != nil {
  •   t.Fatalf("component-scoped duplicate should be valid: %v", err)
    
  • }
  • cfg.DocumentationUnits[0].RelatedUnits = []string{"orders"}
  • // Same-component resolution wins and is therefore not ambiguous.
  • if err := Validate(cfg); err != nil {
  •   t.Fatalf("same-component relation should resolve: %v", err)
    
  • }
    +}

+func TestPublishedV3ExamplesLoad(t *testing.T) {

  • paths := []string{
  •   filepath.Join("..", "..", "wikiforge.example.yaml"),
    
  •   filepath.Join("..", "..", "examples", "wikiforge.yaml"),
    
  •   filepath.Join("..", "assets", "templates", "wikiforge.yaml"),
    
  • }
  • for _, path := range paths {
  •   t.Run(filepath.ToSlash(path), func(t *testing.T) {
    
  •   	cfg, err := Load(path)
    
  •   	if err != nil {
    
  •   		t.Fatalf("load %s: %v", path, err)
    
  •   	}
    
  •   	if cfg.Version != CurrentVersion || cfg.SourceVersion != CurrentVersion {
    
  •   		t.Fatalf("versions=%d/%d", cfg.Version, cfg.SourceVersion)
    
  •   	}
    
  •   	if len(cfg.EnabledComponents()) != 1 || len(cfg.DocumentationUnits) != 2 {
    
  •   		t.Fatalf("unexpected example shape: components=%d units=%d", len(cfg.EnabledComponents()), len(cfg.DocumentationUnits))
    
  •   	}
    
  •   })
    
  • }
    +}

+func TestDocumentationUnitPathsRemainCanonicalAcrossSeparatorStyles(t *testing.T) {

  • dir := t.TempDir()
  • path := filepath.Join(dir, "wikiforge.yaml")
  • content := `version: 3
    +openwiki:
  • command: npx
    +mermaid:
  • mode: basic
    +components:
    • id: app
  • type: microservice
  • repository: ./app
  • enabled: true
    +documentationUnits:
    • id: orders
  • component: app
  • kind: domain
  • sourceRoots: [modules\orders]
  • output: domains\orders
    +system:
  • enabled: false
  • output: ./system
    +`
  • if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
  •   t.Fatal(err)
    
  • }
  • cfg, err := Load(path)
  • if err != nil {
  •   t.Fatal(err)
    
  • }
  • unit := cfg.DocumentationUnits[0]
  • if unit.SourceRoots[0] != "modules/orders" || unit.Output != "domains/orders" {
  •   t.Fatalf("documentation paths are not canonical: roots=%v output=%q", unit.SourceRoots, unit.Output)
    
  • }
    +}

+func TestRejectsCaseOnlyIdentifierAndOutputCollisions(t *testing.T) {

  • root := t.TempDir()
  • cfg := Defaults()
  • cfg.Components = []ComponentConfig{
  •   {ID: "App", Type: "microservice", Profile: "application", Repository: filepath.Join(root, "a"), Enabled: true},
    
  •   {ID: "app", Type: "microservice", Profile: "application", Repository: filepath.Join(root, "b"), Enabled: true},
    
  • }
  • if err := Validate(cfg); err == nil {
  •   t.Fatal("expected case-only component id collision")
    
  • }
  • cfg.Components = []ComponentConfig{{ID: "app", Type: "microservice", Profile: "application", Repository: filepath.Join(root, "app"), Enabled: true}}
  • cfg.DocumentationUnits = []DocumentationUnitConfig{
  •   {ID: "Orders", Component: "app", Kind: "domain", Output: "domains/orders"},
    
  •   {ID: "orders", Component: "app", Kind: "flow", Output: "flows/orders"},
    
  • }
  • if err := Validate(cfg); err == nil {
  •   t.Fatal("expected case-only documentation unit id collision")
    
  • }
  • cfg.DocumentationUnits = []DocumentationUnitConfig{
  •   {ID: "orders-a", Component: "app", Kind: "domain", Output: "Domains/Orders"},
    
  •   {ID: "orders-b", Component: "app", Kind: "domain", Output: "domains/orders"},
    
  • }
  • if err := Validate(cfg); err == nil {
  •   t.Fatal("expected case-only documentation output collision")
    
  • }
    +}

+func TestExplicitEmptyAdaptiveListsArePreserved(t *testing.T) {

  • dir := t.TempDir()
  • path := filepath.Join(dir, "wikiforge.json")
  • content := `{
  • "version": 3,
  • "openwiki": {"command": "npx"},
  • "documentation": {
  • "views": [],
  • "catalogs": {"shardBy": [], "maximumRowsPerPage": 50},
  • "evidence": {"include": [], "exclude": [], "maxFileSizeBytes": 1000}
  • },
  • "mermaid": {"mode": "basic"},
  • "components": [{"id": "app", "type": "microservice", "repository": "./app", "enabled": true}],
  • "system": {"enabled": false, "output": "./system"}
    +}`
  • if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
  •   t.Fatal(err)
    
  • }
  • cfg, err := Load(path)
  • if err != nil {
  •   t.Fatal(err)
    
  • }
  • if cfg.Documentation.Views == nil || len(cfg.Documentation.Views) != 0 {
  •   t.Fatalf("explicit empty views were not preserved: %#v", cfg.Documentation.Views)
    
  • }
  • if cfg.Documentation.Catalogs.ShardBy == nil || len(cfg.Documentation.Catalogs.ShardBy) != 0 {
  •   t.Fatalf("explicit empty shard policy was not preserved: %#v", cfg.Documentation.Catalogs.ShardBy)
    
  • }
  • if cfg.Documentation.Evidence.Include == nil || cfg.Documentation.Evidence.Exclude == nil {
  •   t.Fatalf("explicit empty evidence lists were not preserved: include=%#v exclude=%#v", cfg.Documentation.Evidence.Include, cfg.Documentation.Evidence.Exclude)
    
  • }
    +}

+func TestRejectsUnsupportedShardDimensionAndCriticality(t *testing.T) {

  • cfg := Defaults()
  • cfg.Components = []ComponentConfig{{ID: "app", Type: "microservice", Profile: "application", Repository: t.TempDir(), Enabled: true}}
  • cfg.Documentation.Catalogs.ShardBy = []string{"not-a-dimension"}
  • if err := Validate(cfg); err == nil {
  •   t.Fatal("expected invalid shard dimension")
    
  • }
  • cfg.Documentation.Catalogs.ShardBy = []string{"domain"}
  • cfg.DocumentationUnits = []DocumentationUnitConfig{{ID: "orders", Component: "app", Kind: "domain", Criticality: "urgent"}}
  • if err := Validate(cfg); err == nil {
  •   t.Fatal("expected invalid criticality")
    
  • }
    +}
    diff --git a/internal/config/schema_test.go b/internal/config/schema_test.go
    new file mode 100644
    index 0000000000000000000000000000000000000000..8e4c67b920cd1004099d0d95f4f625949fff9e36
    --- /dev/null
    +++ b/internal/config/schema_test.go
    @@ -0,0 +1,74 @@
    +package config

+import (

  • "encoding/json"
  • "os"
  • "path/filepath"
  • "reflect"
  • "sort"
  • "testing"
    +)

+func TestPublishedSchemaIsValidAndContainsV3PlanningContracts(t *testing.T) {

  • path := filepath.Join("..", "..", "schema", "wikiforge-config.schema.json")
  • data, err := os.ReadFile(path)
  • if err != nil {
  •   t.Fatal(err)
    
  • }
  • var schema map[string]any
  • if err := json.Unmarshal(data, &schema); err != nil {
  •   t.Fatal(err)
    
  • }
  • properties := schema["properties"].(map[string]any)
  • version := properties["version"].(map[string]any)
  • enums := version["enum"].([]any)
  • if enums[len(enums)-1].(float64) != float64(CurrentVersion) {
  •   t.Fatalf("schema versions=%v", enums)
    
  • }
  • if _, ok := properties["documentationUnits"]; !ok {
  •   t.Fatal("schema missing documentationUnits")
    
  • }
  • defs := schema["$defs"].(map[string]any)
  • component := defs["component"].(map[string]any)["properties"].(map[string]any)
  • packs, ok := component["packs"]
  • if !ok {
  •   t.Fatal("schema missing component packs")
    
  • }
  • packItems := packs.(map[string]any)["items"].(map[string]any)["enum"].([]any)

Copy link
Copy Markdown
Owner Author

WIKIFORGE_PHASE1_PATCH_PART 08/19

  • if got, want := stringEnum(packItems), sortedStrings(SupportedCapabilityPacks()); !reflect.DeepEqual(got, want) {
  •   t.Fatalf("schema pack registry drift: got=%v want=%v", got, want)
    
  • }
  • doc := properties["documentation"].(map[string]any)["properties"].(map[string]any)
  • viewItems := doc["views"].(map[string]any)["items"].(map[string]any)["enum"].([]any)
  • if got, want := stringEnum(viewItems), sortedStrings(SupportedViews()); !reflect.DeepEqual(got, want) {
  •   t.Fatalf("schema view registry drift: got=%v want=%v", got, want)
    
  • }
  • for _, field := range []string{"views", "catalogs", "evidence"} {
  •   if _, ok := doc[field]; !ok {
    
  •   	t.Fatalf("schema missing documentation.%s", field)
    
  •   }
    
  • }
  • shardItems := doc["catalogs"].(map[string]any)["properties"].(map[string]any)["shardBy"].(map[string]any)["items"].(map[string]any)["enum"].([]any)
  • if got, want := stringEnum(shardItems), sortedStrings(SupportedShardDimensions()); !reflect.DeepEqual(got, want) {
  •   t.Fatalf("schema shard registry drift: got=%v want=%v", got, want)
    
  • }
  • criticalityItems := defs["documentationUnit"].(map[string]any)["properties"].(map[string]any)["criticality"].(map[string]any)["enum"].([]any)
  • if got, want := stringEnum(criticalityItems), sortedStrings(SupportedCriticalities()); !reflect.DeepEqual(got, want) {
  •   t.Fatalf("schema criticality registry drift: got=%v want=%v", got, want)
    
  • }
    +}

+func stringEnum(values []any) []string {

  • out := make([]string, 0, len(values))
  • for _, value := range values {
  •   out = append(out, value.(string))
    
  • }
  • sort.Strings(out)
  • return out
    +}

+func sortedStrings(values []string) []string {

  • out := append([]string(nil), values...)
  • sort.Strings(out)
  • return out
    +}
    diff --git a/internal/discovery/discovery.go b/internal/discovery/discovery.go
    new file mode 100644
    index 0000000000000000000000000000000000000000..57a8584b567aabbf13ff78f0a36e2238ee9ec4e7
    --- /dev/null
    +++ b/internal/discovery/discovery.go
    @@ -0,0 +1,352 @@
    +package discovery

+import (

  • "crypto/sha256"
  • "encoding/hex"
  • "fmt"
  • "os"
  • "path/filepath"
  • "regexp"
  • "sort"
  • "strings"
  • "github.com/example/wikiforge/internal/config"
  • "github.com/example/wikiforge/internal/model"
    +)

+type rule struct {

  • pack string
  • tokens []string
  • exts []string
    +}

+var rules = []rule{

  • {pack: "api", tokens: []string{"openapi", "swagger", "graphql", "grpc", "protobuf", "endpoint", "controller", "router", "jakarta.ws.rs", "@path", "http handler"}, exts: []string{".proto", ".graphql", ".gql"}},
  • {pack: "messaging", tokens: []string{"kafka", "rabbitmq", "nats", "pubsub", "message broker", "eventbus", "event emitter", "consumer", "producer", "dead letter", "dlq"}},
  • {pack: "workflow", tokens: []string{"camunda", "zeebe", "workflow", "business process", "process engine", "bpmn"}, exts: []string{".bpmn", ".bpmn20.xml"}},
  • {pack: "jobs", tokens: []string{"cron", "scheduler", "scheduled", "jobrunner", "worker pool", "polling", "batch job"}},
  • {pack: "files", tokens: []string{"multipart", "file upload", "file download", "sftp", "object storage", "blob storage", "csv", "xlsx", "pdf generation"}},
  • {pack: "database", tokens: []string{"database", "postgres", "mysql", "oracle", "mongodb", "cassandra", "dynamodb", "schema", "transaction"}, exts: []string{".sql"}},
  • {pack: "migrations", tokens: []string{"migration", "flyway", "liquibase", "seed data", "seeding", "backfill"}},
  • {pack: "data-access", tokens: []string{"repository interface", "repository implementation", "data access", "mybatis", "hibernate", "eclipselink", "jdbc", "entitymanager", "dao", "mapper"}},
  • {pack: "cache", tokens: []string{"cache", "caching", "redis", "memcached", "caffeine", "ttl"}},
  • {pack: "rate-limit", tokens: []string{"rate limit", "ratelimit", "token bucket", "leaky bucket", "sliding window", "quota"}},
  • {pack: "distributed-coordination", tokens: []string{"distributed lock", "leader election", "lease", "fencing token", "semaphore", "coordination", "redlock", "zookeeper", "etcd"}},
  • {pack: "security", tokens: []string{"authentication", "authorization", "oauth", "openid", "oidc", "jwt", "acl", "permission", "rbac", "abac", "mtls"}},
  • {pack: "cryptography", tokens: []string{"encrypt", "decrypt", "cipher", "signature", "hashing", "keystore", "truststore", "certificate", "kms", "hsm"}},
  • {pack: "concurrency", tokens: []string{"thread", "mutex", "semaphore", "executor", "goroutine", "channel", "promise", "future", "coroutine", "reactive", "event loop", "context propagation"}},
  • {pack: "configuration", tokens: []string{"environment variable", "configmap", "configuration", "application.properties", "application.yml", "feature flag", "secret reference"}, exts: []string{".properties", ".env"}},
  • {pack: "container-runtime", tokens: []string{"dockerfile", "kubernetes", "deployment", "statefulset", "helm", "docker compose", "container"}, exts: []string{".dockerfile"}},
  • {pack: "telemetry", tokens: []string{"opentelemetry", "telemetry", "tracing", "metrics", "prometheus", "grafana", "span", "trace id", "structured logging"}},
  • {pack: "domain", tokens: []string{"domain", "business rule", "invariant", "aggregate", "bounded context", "use case", "business flow"}},
  • {pack: "engineering", tokens: []string{"coding standard", "contributing", "lint", "formatter", "architecture decision", "adr"}},
  • {pack: "runtime", tokens: []string{"runtime", "startup", "shutdown", "health check", "readiness", "liveness", "graceful shutdown"}},
    +}

+var unitRootNames = map[string]bool{"domain": true, "domains": true, "module": true, "modules": true, "bounded-context": true, "bounded-contexts": true}
+
+func Discover(cfg config.Config, component config.ComponentConfig) (model.DiscoveryManifest, error) {

  • manifest := model.DiscoveryManifest{
  •   SchemaVersion: 1,
    
  •   Component:     model.Component{ID: component.ID, Type: component.Type, Profile: component.Profile, Repository: component.Repository, Scope: component.Scope, WorkDir: component.WorkDir(), Group: component.Group, Tags: component.Tags, DependsOn: component.DependsOn, Owners: component.Owners, Capabilities: component.Capabilities, Packs: component.Packs},
    
  • }
  • configured := configuredUnits(cfg, component)
  • manifest.Units = append(manifest.Units, configured...)
  • seenUnits := map[string]bool{}
  • coveredRoots := map[string]bool{}
  • for _, unit := range configured {
  •   seenUnits[strings.ToLower(unit.ID)] = true
    
  •   for _, root := range unit.SourceRoots {
    
  •   	coveredRoots[filepath.ToSlash(root)] = true
    
  •   }
    
  • }
  • includePatterns, err := compileGlobs(cfg.Documentation.Evidence.Include)
  • if err != nil {
  •   return manifest, fmt.Errorf("compile evidence include patterns: %w", err)
    
  • }
  • excludePatterns, err := compileGlobs(cfg.Documentation.Evidence.Exclude)
  • if err != nil {
  •   return manifest, fmt.Errorf("compile evidence exclude patterns: %w", err)
    
  • }
  • evidence := map[string]map[string]bool{}
  • hash := sha256.New()
  • workdir := component.WorkDir()
  • err = filepath.WalkDir(workdir, func(path string, entry os.DirEntry, walkErr error) error {
  •   if walkErr != nil {
    
  •   	if filepath.Clean(path) == filepath.Clean(workdir) {
    
  •   		return walkErr
    
  •   	}
    
  •   	rel, _ := filepath.Rel(workdir, path)
    
  •   	manifest.Unknowns = append(manifest.Unknowns, "Unreadable evidence path: "+filepath.ToSlash(rel))
    
  •   	if entry != nil && entry.IsDir() {
    
  •   		return filepath.SkipDir
    
  •   	}
    
  •   	return nil
    
  •   }
    
  •   rel, err := filepath.Rel(workdir, path)
    
  •   if err != nil {
    
  •   	return err
    
  •   }
    
  •   rel = filepath.ToSlash(rel)
    
  •   if rel == "." {
    
  •   	return nil
    
  •   }
    
  •   if entry.IsDir() {
    
  •   	if matchesAny(excludePatterns, rel+"/") {
    
  •   		return filepath.SkipDir
    
  •   	}
    
  •   	return nil
    
  •   }
    
  •   if entry.Type()&os.ModeSymlink != 0 {
    
  •   	return nil
    
  •   }
    
  •   if matchesAny(excludePatterns, rel) || (len(includePatterns) > 0 && !matchesAny(includePatterns, rel)) {
    
  •   	return nil
    
  •   }
    
  •   info, err := entry.Info()
    
  •   if err != nil {
    
  •   	manifest.Unknowns = append(manifest.Unknowns, "Unreadable evidence metadata: "+rel)
    
  •   	return nil
    
  •   }
    
  •   if info.Size() > cfg.Documentation.Evidence.MaxFileSizeBytes {
    
  •   	return nil
    
  •   }
    
  •   b, err := os.ReadFile(path)
    
  •   if err != nil {
    
  •   	manifest.Unknowns = append(manifest.Unknowns, "Unreadable evidence file: "+rel)
    
  •   	return nil
    
  •   }
    
  •   if looksBinary(b) {
    
  •   	return nil
    
  •   }
    
  •   manifest.FilesScanned++
    
  •   manifest.BytesScanned += int64(len(b))
    
  •   _, _ = fmt.Fprintf(hash, "%s\x00", rel)
    
  •   _, _ = hash.Write(b)
    
  •   _, _ = hash.Write([]byte{0})
    
  •   text := strings.ToLower(rel + "\n" + string(b))
    
  •   ext := strings.ToLower(filepath.Ext(rel))
    
  •   for _, candidate := range rules {
    
  •   	if matchesRule(candidate, text, ext) {
    
  •   		if evidence[candidate.pack] == nil {
    
  •   			evidence[candidate.pack] = map[string]bool{}
    
  •   		}
    
  •   		evidence[candidate.pack][rel] = true
    
  •   	}
    
  •   }
    
  •   inferUnitsFromPath(component, rel, seenUnits, coveredRoots, &manifest.Units)
    
  •   return nil
    
  • })
  • if err != nil {
  •   return manifest, err
    
  • }
  • manifest.SourceHash = hex.EncodeToString(hash.Sum(nil))
  • packSet := map[string]bool{}
  • for pack, paths := range evidence {
  •   packSet[pack] = true
    
  •   sorted := keys(paths)
    
  •   manifest.Evidence = append(manifest.Evidence, model.EvidenceMatch{Pack: pack, Paths: sorted, Count: len(sorted)})
    
  • }
  • for _, pack := range component.Packs {
  •   packSet[pack] = true
    
  • }
  • manifest.Packs = keys(packSet)
  • sort.Slice(manifest.Evidence, func(i, j int) bool { return manifest.Evidence[i].Pack < manifest.Evidence[j].Pack })
  • sort.Slice(manifest.Units, func(i, j int) bool { return manifest.Units[i].ID < manifest.Units[j].ID })
  • if manifest.FilesScanned == 0 {
  •   manifest.Unknowns = append(manifest.Unknowns, "No eligible source files were found within the component scope.")
    
  • }
  • if len(manifest.Packs) == 0 {
  •   manifest.Unknowns = append(manifest.Unknowns, "No optional capability packs were discovered from source evidence.")
    
  • }
  • manifest.Unknowns = uniqueStrings(manifest.Unknowns)
  • return manifest, nil
    +}

+func configuredUnits(cfg config.Config, component config.ComponentConfig) []model.DocumentationUnit {

  • var units []model.DocumentationUnit
  • seen := map[string]bool{}
  • for _, unit := range cfg.UnitsForComponent(component.ID) {
  •   units = append(units, model.DocumentationUnit{ID: unit.ID, ComponentID: unit.Component, Kind: unit.Kind, SourceRoots: unit.SourceRoots, RelatedUnits: unit.RelatedUnits, OutputPath: unit.Output, Owners: unit.Owners, Capabilities: unit.Capabilities, Criticality: unit.Criticality, Origin: "configured"})
    
  •   seen[strings.ToLower(unit.ID)] = true
    
  • }
  • for _, capability := range component.Capabilities {
  •   id := portableID(capability)
    
  •   if id == "" || seen[strings.ToLower(id)] {
    
  •   	continue
    
  •   }
    
  •   units = append(units, model.DocumentationUnit{ID: id, ComponentID: component.ID, Kind: "domain", OutputPath: "domains/" + id, Owners: component.Owners, Capabilities: []string{capability}, Origin: "configured-capability"})
    
  •   seen[strings.ToLower(id)] = true
    
  • }
  • return units
    +}

+func inferUnitsFromPath(component config.ComponentConfig, rel string, seen, covered map[string]bool, units *[]model.DocumentationUnit) {

  • parts := strings.Split(rel, "/")
  • for index := 0; index+1 < len(parts); index++ {
  •   rootKind := strings.ToLower(parts[index])
    
  •   if !unitRootNames[rootKind] {
    
  •   	continue
    
  •   }
    
  •   root := strings.Join(parts[:index+2], "/")
    
  •   if coveredBy(root, covered) {
    
  •   	break
    
  •   }
    
  •   id := portableID(parts[index+1])
    
  •   if id != "" && !seen[strings.ToLower(id)] {
    
  •   	kind := "domain"
    
  •   	output := "domains/" + id
    
  •   	if strings.Contains(rootKind, "module") {
    
  •   		kind = "module"
    
  •   		output = "components/" + component.ID + "/modules/" + id
    
  •   	}
    
  •   	*units = append(*units, model.DocumentationUnit{ID: id, ComponentID: component.ID, Kind: kind, SourceRoots: []string{root}, OutputPath: output, Origin: "discovered"})
    
  •   	seen[strings.ToLower(id)] = true
    
  •   }
    
  •   break
    
  • }
  • lower := strings.ToLower(rel)

Copy link
Copy Markdown
Owner Author

WIKIFORGE_PHASE1_PATCH_PART 09/19

  • if strings.HasSuffix(lower, ".bpmn") || strings.HasSuffix(lower, ".bpmn20.xml") {
  •   base := strings.TrimSuffix(filepath.Base(rel), filepath.Ext(rel))
    
  •   base = strings.TrimSuffix(base, ".bpmn20")
    
  •   id := portableID(base)
    
  •   if id == "" || seen[strings.ToLower(id)] {
    
  •   	return
    
  •   }
    
  •   if coveredBy(rel, covered) {
    
  •   	return
    
  •   }
    
  •   *units = append(*units, model.DocumentationUnit{ID: id, ComponentID: component.ID, Kind: "flow", SourceRoots: []string{rel}, OutputPath: "flows/" + id, Origin: "discovered"})
    
  •   seen[strings.ToLower(id)] = true
    
  • }
    +}

+func coveredBy(path string, roots map[string]bool) bool {

  • path = strings.ToLower(strings.TrimSuffix(filepath.ToSlash(path), "/"))
  • for root := range roots {
  •   root = strings.ToLower(strings.TrimSuffix(filepath.ToSlash(root), "/"))
    
  •   if path == root || strings.HasPrefix(path, root+"/") {
    
  •   	return true
    
  •   }
    
  • }
  • return false
    +}

+func matchesRule(candidate rule, text, ext string) bool {

  • for _, wanted := range candidate.exts {
  •   if ext == wanted || strings.HasSuffix(text, wanted) {
    
  •   	return true
    
  •   }
    
  • }
  • for _, token := range candidate.tokens {
  •   if strings.Contains(text, token) {
    
  •   	return true
    
  •   }
    
  • }
  • return false
    +}

+func compileGlobs(patterns []string) ([]*regexp.Regexp, error) {

  • compiled := make([]*regexp.Regexp, 0, len(patterns))
  • for _, pattern := range patterns {
  •   pattern = filepath.ToSlash(strings.TrimSpace(pattern))
    
  •   if pattern == "" {
    
  •   	continue
    
  •   }
    
  •   expression := globExpression(pattern)
    
  •   re, err := regexp.Compile(expression)
    
  •   if err != nil {
    
  •   	return nil, fmt.Errorf("invalid glob %q: %w", pattern, err)
    
  •   }
    
  •   compiled = append(compiled, re)
    
  • }
  • return compiled, nil
    +}

+func matchesAny(patterns []*regexp.Regexp, value string) bool {

  • value = filepath.ToSlash(value)
  • for _, pattern := range patterns {
  •   if pattern.MatchString(value) {
    
  •   	return true
    
  •   }
    
  • }
  • return false
    +}

+func globExpression(pattern string) string {

  • var b strings.Builder
  • b.WriteString("^")
  • for i := 0; i < len(pattern); i++ {
  •   switch pattern[i] {
    
  •   case '*':
    
  •   	if i+1 < len(pattern) && pattern[i+1] == '*' {
    
  •   		if i+2 < len(pattern) && pattern[i+2] == '/' {
    
  •   			b.WriteString("(?:.*/)?")
    
  •   			i += 2
    
  •   		} else {
    
  •   			b.WriteString(".*")
    
  •   			i++
    
  •   		}
    
  •   	} else {
    
  •   		b.WriteString("[^/]*")
    
  •   	}
    
  •   case '?':
    
  •   	b.WriteString("[^/]")
    
  •   default:
    
  •   	b.WriteString(regexp.QuoteMeta(string(pattern[i])))
    
  •   }
    
  • }
  • b.WriteString("$")
  • return b.String()
    +}

+func looksBinary(b []byte) bool {

  • limit := len(b)
  • if limit > 8192 {
  •   limit = 8192
    
  • }
  • for _, c := range b[:limit] {
  •   if c == 0 {
    
  •   	return true
    
  •   }
    
  • }
  • return false
    +}

+func portableID(value string) string {

  • value = strings.ToLower(strings.TrimSpace(value))
  • var b strings.Builder
  • lastDash := false
  • for _, r := range value {
  •   if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
    
  •   	b.WriteRune(r)
    
  •   	lastDash = false
    
  •   	continue
    
  •   }
    
  •   if !lastDash {
    
  •   	b.WriteByte('-')
    
  •   	lastDash = true
    
  •   }
    
  • }
  • return strings.Trim(b.String(), "-")
    +}

+func uniqueStrings(values []string) []string {

  • set := map[string]bool{}
  • for _, value := range values {
  •   if strings.TrimSpace(value) != "" {
    
  •   	set[value] = true
    
  •   }
    
  • }
  • return keys(set)
    +}

+func keys(values map[string]bool) []string {

  • out := make([]string, 0, len(values))
  • for value := range values {
  •   out = append(out, value)
    
  • }
  • sort.Strings(out)
  • return out
    +}
    diff --git a/internal/discovery/discovery_test.go b/internal/discovery/discovery_test.go
    new file mode 100644
    index 0000000000000000000000000000000000000000..c0ed8732eb595b5e681462ee1aba9df231f05510
    --- /dev/null
    +++ b/internal/discovery/discovery_test.go
    @@ -0,0 +1,142 @@
    +package discovery

+import (

  • "os"
  • "path/filepath"
  • "reflect"
  • "strings"
  • "testing"
  • "github.com/example/wikiforge/internal/config"
  • "github.com/example/wikiforge/internal/model"
    +)

+func TestDiscoverDeterministicCapabilitiesUnitsAndEvidenceFilters(t *testing.T) {

  • root := t.TempDir()
  • write(t, root, "modules/order/OrderResource.java", @Path("/orders") class OrderResource { KafkaProducer producer; RedisCache cache; })
  • write(t, root, "workflows/order/submit-order.bpmn", <definitions><process id="submit-order"/></definitions>)
  • write(t, root, "db/migration/V1__orders.sql", create table orders(id bigint primary key);)
  • write(t, root, "deploy/deployment.yaml", kind: Deployment)
  • write(t, root, "generated/ignored.sql", create table should_not_be_seen(id int);)
  • write(t, root, "blob.bin", "must be excluded by root-level double-star glob")
  • outside := filepath.Join(t.TempDir(), "outside.txt")
  • if err := os.WriteFile(outside, []byte("opentelemetry should not be followed"), 0o644); err != nil {
  •   t.Fatal(err)
    
  • }
  • _ = os.Symlink(outside, filepath.Join(root, "external-link.txt")) // unsupported platforms may reject; discovery must skip it when present.
  • cfg := config.Defaults()
  • cfg.Components = []config.ComponentConfig{{ID: "commerce", Type: "modular-monolith", Profile: "modular-application", Repository: root, Enabled: true, Packs: []string{"telemetry"}, Capabilities: []string{"pricing"}}}
  • cfg.DocumentationUnits = []config.DocumentationUnitConfig{{ID: "order-management", Component: "commerce", Kind: "domain", SourceRoots: []string{"modules/order", "workflows/order"}, Output: "domains/order-management"}}
  • component := cfg.Components[0]
  • first, err := Discover(cfg, component)
  • if err != nil {
  •   t.Fatal(err)
    
  • }
  • second, err := Discover(cfg, component)
  • if err != nil {
  •   t.Fatal(err)
    
  • }
  • if !reflect.DeepEqual(first, second) {
  •   t.Fatalf("discovery is not deterministic:\n%+v\n%+v", first, second)
    
  • }
  • for _, pack := range []string{"api", "cache", "container-runtime", "database", "messaging", "migrations", "telemetry", "workflow"} {
  •   if !contains(first.Packs, pack) {
    
  •   	t.Errorf("missing pack %s in %v", pack, first.Packs)
    
  •   }
    
  • }
  • if first.FilesScanned != 4 {
  •   t.Fatalf("files scanned=%d, want 4", first.FilesScanned)
    
  • }
  • if !hasUnit(first, "order-management", "configured") || !hasUnit(first, "pricing", "configured-capability") {
  •   t.Fatalf("unexpected units: %+v", first.Units)
    
  • }
  • if hasUnit(first, "submit-order", "discovered") {
  •   t.Fatal("BPMN covered by configured source root must not create a duplicate flow unit")
    
  • }
  • oldHash := first.SourceHash
  • write(t, root, "modules/order/OrderResource.java", @Path("/orders") class OrderResource { KafkaProducer producer; RedisCache cache; Meter meter; })
  • changed, err := Discover(cfg, component)
  • if err != nil {
  •   t.Fatal(err)
    
  • }
  • if changed.SourceHash == oldHash {
  •   t.Fatal("source hash did not change")
    
  • }
    +}

+func TestDiscoverInfersModuleAndFlowUnits(t *testing.T) {

  • root := t.TempDir()
  • write(t, root, "src/modules/catalog/README.md", "domain module")
  • write(t, root, "processes/reconcile.bpmn", "")
  • cfg := config.Defaults()
  • cfg.Components = []config.ComponentConfig{{ID: "app", Type: "microservice", Profile: "application", Repository: root, Enabled: true}}
  • manifest, err := Discover(cfg, cfg.Components[0])
  • if err != nil {
  •   t.Fatal(err)
    
  • }
  • if !hasUnit(manifest, "catalog", "discovered") || !hasUnit(manifest, "reconcile", "discovered") {
  •   t.Fatalf("units=%+v", manifest.Units)
    
  • }
    +}

+func write(t *testing.T, root, rel, content string) {

  • t.Helper()
  • path := filepath.Join(root, filepath.FromSlash(rel))
  • if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
  •   t.Fatal(err)
    
  • }
  • if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
  •   t.Fatal(err)
    
  • }
    +}
    +func contains(values []string, wanted string) bool {
  • for _, value := range values {
  •   if value == wanted {
    
  •   	return true
    
  •   }
    
  • }
  • return false
    +}
    +func hasUnit(manifest model.DiscoveryManifest, id, origin string) bool {
  • for _, unit := range manifest.Units {
  •   if unit.ID == id && unit.Origin == origin {
    
  •   	return true
    
  •   }
    
  • }
  • return false
    +}

+func TestConfiguredUnitsSuppressCaseEquivalentCapabilityAndInferredUnits(t *testing.T) {

  • root := t.TempDir()
  • write(t, root, "modules/orders/README.md", "domain")
  • cfg := config.Defaults()
  • component := config.ComponentConfig{ID: "app", Type: "modular-monolith", Profile: "modular-application", Repository: root, Enabled: true, Capabilities: []string{"orders"}}
  • cfg.Components = []config.ComponentConfig{component}
  • cfg.DocumentationUnits = []config.DocumentationUnitConfig{{ID: "Orders", Component: "app", Kind: "domain", SourceRoots: []string{"modules/orders"}}}
  • manifest, err := Discover(cfg, component)
  • if err != nil {
  •   t.Fatal(err)
    
  • }
  • count := 0
  • for _, unit := range manifest.Units {
  •   if strings.EqualFold(unit.ID, "orders") {
    
  •   	count++
    
  •   }
    
  • }
  • if count != 1 {
  •   t.Fatalf("case-equivalent configured/capability/inferred units were not deduplicated: %+v", manifest.Units)
    
  • }
    +}

+func TestDiscoverFailsWhenComponentRootDoesNotExist(t *testing.T) {

  • root := filepath.Join(t.TempDir(), "missing")
  • cfg := config.Defaults()
  • component := config.ComponentConfig{ID: "app", Type: "microservice", Profile: "application", Repository: root, Enabled: true}
  • cfg.Components = []config.ComponentConfig{component}
  • if _, err := Discover(cfg, component); err == nil {
  •   t.Fatal("expected missing component root error")
    
  • }
    +}
    diff --git a/internal/model/model.go b/internal/model/model.go
    index dee8817f8da0e8663c42d1842cdaa4385a8ec052..50354fc39a8aaf705998c31fa5f57b51aae27b3d 100644
    --- a/internal/model/model.go
    +++ b/internal/model/model.go
    @@ -3,15 +3,18 @@ package model
    import "time"

type Component struct {

  • ID string json:"id"
  • Type string json:"type"
  • Profile string json:"profile"
  • Repository string json:"repository"
  • Scope string json:"scope,omitempty"
  • WorkDir string json:"workDir"
  • Group string json:"group,omitempty"
  • Tags []string json:"tags,omitempty"
  • DependsOn []string json:"dependsOn,omitempty"
  • ID string json:"id"
  • Type string json:"type"
  • Profile string json:"profile"
  • Repository string json:"repository"
  • Scope string json:"scope,omitempty"
  • WorkDir string json:"workDir"
  • Group string json:"group,omitempty"
  • Tags []string json:"tags,omitempty"
  • DependsOn []string json:"dependsOn,omitempty"
  • Owners []string json:"owners,omitempty"
  • Capabilities []string json:"capabilities,omitempty"
  • Packs []string json:"packs,omitempty"
    }

type PageContract struct {
@@ -61,11 +64,13 @@ type PhaseStatus struct {
}

type TargetState struct {

  • GitHead string json:"gitHead,omitempty"
  • DocsHash string json:"docsHash,omitempty"
  • SourceHash string json:"sourceHash,omitempty"
  • Status string json:"status"
  • Phases map[string]PhaseStatus json:"phases"
  • GitHead string json:"gitHead,omitempty"
  • DocsHash string json:"docsHash,omitempty"
  • SourceHash string json:"sourceHash,omitempty"
  • DiscoveryHash string json:"discoveryHash,omitempty"
  • PlanHash string json:"planHash,omitempty"
  • Status string json:"status"
  • Phases map[string]PhaseStatus json:"phases"
    }

type RunState struct {
@@ -79,3 +84,65 @@ type RunState struct {
Services map[string]TargetState json:"services,omitempty"
System TargetState json:"system"
}
+
+type DocumentationUnit struct {

  • ID string json:"id"
  • ComponentID string json:"componentId"
  • Kind string json:"kind"
  • SourceRoots []string json:"sourceRoots,omitempty"
  • RelatedUnits []string json:"relatedUnits,omitempty"
  • OutputPath string json:"outputPath,omitempty"
  • Owners []string json:"owners,omitempty"

Copy link
Copy Markdown
Owner Author

WIKIFORGE_PHASE1_PATCH_PART 10/19

  • Capabilities []string json:"capabilities,omitempty"
  • Criticality string json:"criticality,omitempty"
  • Origin string json:"origin"
    +}

+type EvidenceMatch struct {

  • Pack string json:"pack"
  • Paths []string json:"paths"
  • Count int json:"count"
    +}

+type DiscoveryManifest struct {

  • SchemaVersion int json:"schemaVersion"
  • Component Component json:"component"
  • SourceHash string json:"sourceHash"
  • FilesScanned int json:"filesScanned"
  • BytesScanned int64 json:"bytesScanned"
  • Packs []string json:"packs"
  • Evidence []EvidenceMatch json:"evidence"
  • Units []DocumentationUnit json:"units"
  • Unknowns []string json:"unknowns,omitempty"
    +}

+type PlanPage struct {

  • ID string json:"id"
  • Path string json:"path"
  • View string json:"view"
  • Pack string json:"pack,omitempty"
  • UnitID string json:"unitId,omitempty"
  • Kind string json:"kind"
  • Reason string json:"reason"
  • ShardBy []string json:"shardBy,omitempty"
  • MaximumRowsPerPage int json:"maximumRowsPerPage,omitempty"
    +}

+type PlanDecision struct {

  • Subject string json:"subject"
  • Action string json:"action"
  • Reason string json:"reason"
    +}

+type DocumentationPlan struct {

  • SchemaVersion int json:"schemaVersion"
  • ComponentID string json:"componentId"
  • Profile string json:"profile"
  • Views []string json:"views"
  • SelectedPacks []string json:"selectedPacks"
  • Units []DocumentationUnit json:"units"
  • Pages []PlanPage json:"pages"
  • Decisions []PlanDecision json:"decisions"
  • ShardBy []string json:"shardBy,omitempty"
  • MaximumRowsPerPage int json:"maximumRowsPerPage,omitempty"
    +}
    diff --git a/internal/openwiki/runner_test.go b/internal/openwiki/runner_test.go
    index e10d33dcdf1b2dffbaa995ab4c8aadf17138aeea..346100170c3b6bea9d5ade82af7f2a95fa9cfb7b 100644
    --- a/internal/openwiki/runner_test.go
    +++ b/internal/openwiki/runner_test.go
    @@ -140,8 +140,8 @@ func TestExecRunnerRejectsClarificationResponse(t *testing.T) {
    Args: []string{"-test.run=TestOpenWikiHelperProcess", "--", "code"},
    TimeoutMinutes: 1,
    Environment: map[string]string{
  •   	"WIKIFORGE_HELPER_PROCESS":       "1",
    
  •   	"WIKIFORGE_CLARIFICATION_TEST":   "1",
    
  •   	"WIKIFORGE_HELPER_PROCESS":      "1",
    
  •   	"WIKIFORGE_CLARIFICATION_TEST":  "1",
      	"WIKIFORGE_CAPTURE_PROMPT_PATH": filepath.Join(t.TempDir(), "prompt.txt"),
      },
    

    }}
    diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go
    index 15702006eae607755dbd5dad02508bb9ecf96675..2fc96dac600beb83c52add7804350e6db034e529 100644
    --- a/internal/orchestrator/orchestrator.go
    +++ b/internal/orchestrator/orchestrator.go
    @@ -16,9 +16,11 @@ import (
    "time"

    "github.com/example/wikiforge/internal/config"

  • "github.com/example/wikiforge/internal/discovery"
    "github.com/example/wikiforge/internal/graph"
    "github.com/example/wikiforge/internal/model"
    "github.com/example/wikiforge/internal/openwiki"

  • "github.com/example/wikiforge/internal/planner"
    "github.com/example/wikiforge/internal/prompts"
    "github.com/example/wikiforge/internal/report"
    "github.com/example/wikiforge/internal/state"
    @@ -60,23 +62,174 @@ func New(cfg config.Config, runner openwiki.Runner, out io.Writer) *Orchestrator
    }

func (o *Orchestrator) Plan(componentID string, includeSystem bool) []string {

  • lines, err := o.PlanWithExplain(componentID, includeSystem, true)
  • if err != nil {
  •   return []string{"plan failed: " + err.Error()}
    
  • }
  • return lines
    +}

+func (o *Orchestrator) PlanWithExplain(componentID string, includeSystem, explain bool) ([]string, error) {

  • components := o.selectedComponents(componentID)
  • if len(components) == 0 {
  •   return nil, fmt.Errorf("no enabled component matched %q", componentID)
    
  • }
    var lines []string
  • for _, component := range o.selectedComponents(componentID) {
  • var systemComponentPlans []model.DocumentationPlan
  • for _, component := range components {
  •   manifest, plan, err := o.prepareAdaptivePlan(component, true)
    
  •   if err != nil {
    
  •   	return nil, fmt.Errorf("component %s discovery failed: %w", component.ID, err)
    
  •   }
    
  •   if component.IsIncludedInSystem() {
    
  •   	systemComponentPlans = append(systemComponentPlans, plan)
    
  •   }
    
  •   lines = append(lines, fmt.Sprintf("component %s type=%s profile=%s repository=%s scope=%s scanned=%d", component.ID, component.Type, component.Profile, component.Repository, printableScope(component.Scope), manifest.FilesScanned))
    
  •   if explain {
    
  •   	for _, line := range planner.Explain(plan) {
    
  •   		lines = append(lines, "  "+line)
    
  •   	}
    
  •   } else {
    
  •   	lines = append(lines, fmt.Sprintf("  adaptive plan packs=%d units=%d pages=%d; use --explain for decisions", len(plan.SelectedPacks), len(plan.Units), len(plan.Pages)))
    
  •   }
      profile, _ := prompts.GetProfile(component.Profile)
    
  •   lines = append(lines, fmt.Sprintf("component %s type=%s profile=%s repository=%s scope=%s", component.ID, component.Type, component.Profile, component.Repository, printableScope(component.Scope)))
    
  •   lines = append(lines, "  execution compatibility phases:")
      for _, p := range profile.Phases {
    
  •   	lines = append(lines, "  "+p.ID+"  "+p.Name)
    
  •   	lines = append(lines, "    "+p.ID+"  "+p.Name)
      }
    
  •   lines = append(lines, "  validate -> targeted repair -> graph export")
    
  •   lines = append(lines, "    validate -> targeted repair -> graph export")
    
    }
    if includeSystem && o.Config.System.Enabled {
  •   lines = append(lines, fmt.Sprintf("system %s (%s)", o.Config.System.ID, o.Config.System.Output))
    
  •   for _, p := range prompts.SystemPhases {
    
  •   	lines = append(lines, "  "+p.ID+"  "+p.Name)
    
  •   if len(systemComponentPlans) == 0 {
    
  •   	_ = os.Remove(filepath.Join(o.Config.Workspace, ".wikiforge", "system", "plan.json"))
    
  •   	lines = append(lines, fmt.Sprintf("system %s skipped: no selected component is included in system aggregation", o.Config.System.ID))
    
  •   } else {
    
  •   	systemPlan := planner.BuildSystem(o.Config, systemComponentPlans)
    
  •   	if err := writeJSON(filepath.Join(o.Config.Workspace, ".wikiforge", "system", "plan.json"), systemPlan); err != nil {
    
  •   		return nil, fmt.Errorf("persist system %s plan: %w", o.Config.System.ID, err)
    
  •   	}
    
  •   	lines = append(lines, fmt.Sprintf("system %s (%s)", o.Config.System.ID, o.Config.System.Output))
    
  •   	if explain {
    
  •   		for _, line := range planner.Explain(systemPlan) {
    
  •   			lines = append(lines, "  "+line)
    
  •   		}
    
  •   	} else {
    
  •   		lines = append(lines, fmt.Sprintf("  adaptive system plan packs=%d units=%d pages=%d; use --explain for decisions", len(systemPlan.SelectedPacks), len(systemPlan.Units), len(systemPlan.Pages)))
    
  •   	}
    
  •   	lines = append(lines, "  execution compatibility phases:")
    
  •   	for _, p := range prompts.SystemPhases {
    
  •   		lines = append(lines, "    "+p.ID+"  "+p.Name)
    
  •   	}
    
  •   	lines = append(lines, "    validate -> targeted repair -> graph export")
      }
    
  •   lines = append(lines, "  validate -> targeted repair -> graph export")
    
    }
  • return lines
  • return lines, nil
    +}

+func (o *Orchestrator) Discover(componentID string, persist bool) ([]model.DiscoveryManifest, error) {

  • components := o.selectedComponents(componentID)
  • if len(components) == 0 {
  •   return nil, fmt.Errorf("no enabled component matched %q", componentID)
    
  • }
  • out := make([]model.DiscoveryManifest, 0, len(components))
  • for _, component := range components {
  •   manifest, _, err := o.prepareAdaptivePlan(component, persist)
    
  •   if err != nil {
    
  •   	return nil, fmt.Errorf("component %s: %w", component.ID, err)
    
  •   }
    
  •   out = append(out, manifest)
    
  • }
  • return out, nil
    +}

+func (o *Orchestrator) AdaptivePlans(componentID string, persist bool) ([]model.DocumentationPlan, error) {

  • components := o.selectedComponents(componentID)
  • if len(components) == 0 {
  •   return nil, fmt.Errorf("no enabled component matched %q", componentID)
    
  • }
  • out := make([]model.DocumentationPlan, 0, len(components))
  • for _, component := range components {
  •   _, plan, err := o.prepareAdaptivePlan(component, persist)
    
  •   if err != nil {
    
  •   	return nil, fmt.Errorf("component %s: %w", component.ID, err)
    
  •   }
    
  •   out = append(out, plan)
    
  • }
  • return out, nil
    +}

+func (o *Orchestrator) prepareAdaptivePlan(component config.ComponentConfig, persist bool) (model.DiscoveryManifest, model.DocumentationPlan, error) {

  • manifest, err := discovery.Discover(o.Config, component)
  • if err != nil {
  •   return model.DiscoveryManifest{}, model.DocumentationPlan{}, err
    
  • }
  • plan := planner.Build(o.Config, component, manifest)
  • if persist {
  •   root := filepath.Join(o.Config.Workspace, ".wikiforge", "components", component.ID)
    
  •   if err := writeJSON(filepath.Join(root, "discovery.json"), manifest); err != nil {
    
  •   	return manifest, plan, err
    
  •   }
    
  •   if err := writeJSON(filepath.Join(root, "plan.json"), plan); err != nil {
    
  •   	return manifest, plan, err
    
  •   }
    
  • }
  • return manifest, plan, nil
    +}

+func (o *Orchestrator) prepareSystemAdaptivePlan(components []config.ComponentConfig, persist bool) (model.DocumentationPlan, error) {

  • componentPlans := make([]model.DocumentationPlan, 0, len(components))
  • for _, component := range components {
  •   _, plan, err := o.prepareAdaptivePlan(component, persist)
    
  •   if err != nil {
    
  •   	return model.DocumentationPlan{}, fmt.Errorf("component %s: %w", component.ID, err)
    
  •   }
    
  •   componentPlans = append(componentPlans, plan)
    
  • }
  • systemPlan := planner.BuildSystem(o.Config, componentPlans)
  • if persist {
  •   if err := writeJSON(filepath.Join(o.Config.Workspace, ".wikiforge", "system", "plan.json"), systemPlan); err != nil {
    
  •   	return systemPlan, err
    
  •   }
    
  • }
  • return systemPlan, nil
    +}

+func writeJSON(path string, value any) error {

  • if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
  •   return err
    
  • }
  • data, err := json.MarshalIndent(value, "", " ")
  • if err != nil {
  •   return err
    
  • }
  • data = append(data, '\n')
  • tmp := path + ".tmp"
  • if err := os.WriteFile(tmp, data, 0o644); err != nil {
  •   return err
    
  • }
  • if err := os.Rename(tmp, path); err == nil {
  •   return nil
    
  • }
  • // Windows does not replace an existing destination. Preserve atomic rename
  • // on hosts that support it, then use a remove-and-retry fallback.
  • if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
  •   _ = os.Remove(tmp)
    
  •   return err
    
  • }
  • if err := os.Rename(tmp, path); err != nil {
  •   _ = os.Remove(tmp)
    
  •   return err
    
  • }
  • return nil
    +}

+func hashValue(value any) string {

  • data, err := json.Marshal(value)
  • if err != nil {
  •   return ""
    
  • }
  • sum := sha256.Sum256(data)
  • return hex.EncodeToString(sum[:])
    }

func (o *Orchestrator) Generate(ctx context.Context, options GenerateOptions) (GenerateResult, error) {
@@ -88,15 +241,19 @@ func (o *Orchestrator) Generate(ctx context.Context, options GenerateOptions) (G
return GenerateResult{}, err
}
migrateState(&st)

  • if st.RunID == "" || (!options.Resume && !options.UpdateOnly) {
  • if st.RunID == "" {
    st = model.RunState{
  •   	Version:    2,
    
  •   	RunID:      time.Now().UTC().Format("20060102T150405Z"),
    
  •   	Mode:       "generate",
    
  •   	StartedAt:  time.Now().UTC(),
    
  •   	Version:    3,
      	Components: map[string]model.TargetState{},
      	System:     model.TargetState{Phases: map[string]model.PhaseStatus{}},
      }
    
  • }
  • if !options.Resume && !options.UpdateOnly {
  •   // A full generation starts a new reportable run, but the target hashes
    
  •   // remain the last-successful checkpoint until each target completes.
    
  •   st.RunID = time.Now().UTC().Format("20060102T150405.000000000Z")
    
  •   st.Mode = "generate"
    
  •   st.StartedAt = time.Now().UTC()
    

Copy link
Copy Markdown
Owner Author

WIKIFORGE_PHASE1_PATCH_PART 11/19
} else if options.UpdateOnly {
// Start a new reportable run while preserving the last successful hashes
// and phase state required for scoped no-op detection.
@@ -206,7 +363,21 @@ func (o *Orchestrator) runComponent(ctx context.Context, st *model.RunState, com
if err != nil {
return model.ValidationResult{}, err
}

  • if err := o.writeComponentInstructions(component, profile); err != nil {
  • manifest, adaptivePlan, err := o.prepareAdaptivePlan(component, true)
  • if err != nil {
  •   return model.ValidationResult{}, fmt.Errorf("prepare adaptive plan: %w", err)
    
  • }
  • adaptiveValues := prompts.AdaptiveValues(manifest, adaptivePlan)
  • artifactRoot := filepath.Join(o.Config.Workspace, ".wikiforge", "components", component.ID)
  • if rel, relErr := filepath.Rel(component.WorkDir(), filepath.Join(artifactRoot, "discovery.json")); relErr == nil {
  •   adaptiveValues["DISCOVERY_ARTIFACT"] = filepath.ToSlash(rel)
    
  • }
  • if rel, relErr := filepath.Rel(component.WorkDir(), filepath.Join(artifactRoot, "plan.json")); relErr == nil {
  •   adaptiveValues["PLAN_ARTIFACT"] = filepath.ToSlash(rel)
    
  • }
  • discoveryHash := hashValue(manifest)
  • planHash := hashValue(adaptivePlan)
  • if err := o.writeComponentInstructions(component, profile, manifest, adaptivePlan, adaptiveValues); err != nil {
    return model.ValidationResult{}, err
    }
    removeObsoleteDocumentation(component.DocumentationRoot(), []string{
    @@ -227,22 +398,27 @@ func (o *Orchestrator) runComponent(ctx context.Context, st *model.RunState, com
    }
    previousSourceHash := target.SourceHash
    previousDocsHash := target.DocsHash
  • previousDiscoveryHash := target.DiscoveryHash
  • previousPlanHash := target.PlanHash
    currentSourceHash := componentSourceHash(workdir)
    currentDocsHash := directoryHash(component.DocumentationRoot())
    target.Status = "running"
    target.GitHead = gitHead(component.Repository)
  • target.SourceHash = currentSourceHash
  • // Source, discovery, and plan hashes remain the last-successful checkpoint
  • // until generation, validation, and finalization complete.
    o.saveComponentTarget(st, component.ID, target)
  • if options.UpdateOnly && previousSourceHash != "" && previousSourceHash == currentSourceHash && previousDocsHash != "" && previousDocsHash == currentDocsHash {
  • if options.UpdateOnly && previousSourceHash != "" && previousSourceHash == currentSourceHash && previousDocsHash != "" && previousDocsHash == currentDocsHash && previousDiscoveryHash == discoveryHash && previousPlanHash == planHash {
    progress.skip("UPD", "No scoped source or documentation changes; model call skipped")
    } else if options.UpdateOnly {
  •   prompt, err := prompts.RenderComponentUpdate(profile, component, o.Config.Documentation.Language)
    
  •   prompt, err := prompts.RenderComponentUpdateWithValues(profile, component, o.Config.Documentation.Language, adaptiveValues)
      if err != nil {
      	return model.ValidationResult{}, err
      }
      label := progress.start("UPD", "Incremental documentation update")
      if err := o.runWithRetries(ctx, workdir, "update", prompt, label); err != nil {
    
  •   	target.Status = "failed"
    
  •   	o.saveComponentTarget(st, component.ID, target)
      	progress.fail("UPD", err.Error())
      	return model.ValidationResult{}, err
      }
    

@@ -260,7 +436,7 @@ func (o *Orchestrator) runComponent(ctx context.Context, st *model.RunState, com
target.Phases[phase.ID] = ps
o.saveComponentTarget(st, component.ID, target)

  •   	prompt, err := prompts.RenderComponentPhase(phase, profile, component, o.Config.Documentation.Language, nil)
    
  •   	prompt, err := prompts.RenderComponentPhase(phase, profile, component, o.Config.Documentation.Language, adaptiveValues)
      	if err != nil {
      		progress.fail(phase.ID, err.Error())
      		return model.ValidationResult{}, err
    

@@ -273,6 +449,7 @@ func (o *Orchestrator) runComponent(ctx context.Context, st *model.RunState, com
if err := o.runWithRetries(ctx, workdir, op, prompt, label); err != nil {
ps.Status = "failed"
ps.Error = err.Error()

  •   		target.Status = "failed"
      		target.Phases[phase.ID] = ps
      		o.saveComponentTarget(st, component.ID, target)
      		progress.fail(phase.ID, err.Error())
    

@@ -327,6 +504,8 @@ func (o *Orchestrator) runComponent(ctx context.Context, st *model.RunState, com
target.GitHead = gitHead(component.Repository)
target.SourceHash = componentSourceHash(workdir)
target.DocsHash = directoryHash(component.DocumentationRoot())

  • target.DiscoveryHash = discoveryHash
  • target.PlanHash = planHash
    if vr.Accepted {
    target.Status = "completed"
    } else {
    @@ -342,10 +521,11 @@ func (o *Orchestrator) runComponent(ctx context.Context, st *model.RunState, com

func (o *Orchestrator) runSystem(ctx context.Context, st *model.RunState, components []config.ComponentConfig, options GenerateOptions) (model.ValidationResult, error) {
root := o.Config.System.Output

  • if err := o.prepareSystemWorkspace(root, components); err != nil {
  • systemPlan, err := o.prepareSystemWorkspace(root, components)
  • if err != nil {
    return model.ValidationResult{}, err
    }
  • if err := o.writeSystemInstructions(root); err != nil {
  • if err := o.writeSystemInstructions(root, systemPlan); err != nil {
    return model.ValidationResult{}, err
    }
    removeObsoleteDocumentation(filepath.Join(root, "openwiki"), []string{
    @@ -369,21 +549,25 @@ func (o *Orchestrator) runSystem(ctx context.Context, st *model.RunState, compon
    }
    previousSourceHash := target.SourceHash
    previousDocsHash := target.DocsHash
  • previousPlanHash := target.PlanHash
  • currentPlanHash := hashValue(systemPlan)
    currentSourceHash := directoryHash(filepath.Join(root, "sources")) + directoryHash(filepath.Join(root, "facts"))
    currentDocsHash := directoryHash(filepath.Join(root, "openwiki"))
    target.Status = "running"
    target.GitHead = gitHead(root)
    o.saveSystemTarget(st, target)
  • if options.UpdateOnly && previousSourceHash != "" && previousSourceHash == currentSourceHash && previousDocsHash != "" && previousDocsHash == currentDocsHash {
  • if options.UpdateOnly && previousSourceHash != "" && previousSourceHash == currentSourceHash && previousDocsHash != "" && previousDocsHash == currentDocsHash && previousPlanHash == currentPlanHash {
    progress.skip("UPD", "No source or documentation changes; model call skipped")
    } else if options.UpdateOnly {
  •   prompt, err := prompts.RenderSystemUpdate(o.Config.Documentation.Language, o.Config.System.ID)
    
  •   prompt, err := prompts.RenderSystemUpdateWithPlan(o.Config.Documentation.Language, o.Config.System.ID, systemPlan)
      if err != nil {
      	return model.ValidationResult{}, err
      }
      label := progress.start("UPD", "Incremental whole-system update")
      if err := o.runWithRetries(ctx, root, "update", prompt, label); err != nil {
    
  •   	target.Status = "failed"
    
  •   	o.saveSystemTarget(st, target)
      	progress.fail("UPD", err.Error())
      	return model.ValidationResult{}, err
      }
    

@@ -401,7 +585,7 @@ func (o *Orchestrator) runSystem(ctx context.Context, st *model.RunState, compon
target.Phases[phase.ID] = ps
o.saveSystemTarget(st, target)

  •   	prompt, err := prompts.RenderSystemPhase(phase, o.Config.Documentation.Language, o.Config.System.ID)
    
  •   	prompt, err := prompts.RenderSystemPhaseWithPlan(phase, o.Config.Documentation.Language, o.Config.System.ID, systemPlan)
      	if err != nil {
      		progress.fail(phase.ID, err.Error())
      		return model.ValidationResult{}, err
    

@@ -414,6 +598,7 @@ func (o *Orchestrator) runSystem(ctx context.Context, st *model.RunState, compon
if err := o.runWithRetries(ctx, root, op, prompt, label); err != nil {
ps.Status = "failed"
ps.Error = err.Error()

  •   		target.Status = "failed"
      		target.Phases[phase.ID] = ps
      		o.saveSystemTarget(st, target)
      		progress.fail(phase.ID, err.Error())
    

@@ -464,6 +649,7 @@ func (o *Orchestrator) runSystem(ctx context.Context, st *model.RunState, compon
target.GitHead = gitHead(root)
target.SourceHash = directoryHash(filepath.Join(root, "sources")) + directoryHash(filepath.Join(root, "facts"))
target.DocsHash = directoryHash(filepath.Join(root, "openwiki"))

  • target.PlanHash = currentPlanHash
    if vr.Accepted {
    target.Status = "completed"
    } else {
    @@ -508,16 +694,16 @@ func (o *Orchestrator) runWithRetries(ctx context.Context, workdir, operation, p
    return last
    }

-func (o *Orchestrator) writeComponentInstructions(component config.ComponentConfig, profile prompts.Profile) error {

  • content, err := prompts.RenderInstructions(profile, component, o.Config.Documentation.Language)
    +func (o *Orchestrator) writeComponentInstructions(component config.ComponentConfig, profile prompts.Profile, manifest model.DiscoveryManifest, plan model.DocumentationPlan, values map[string]string) error {
  • content, err := prompts.RenderInstructionsWithPlanValues(profile, component, o.Config.Documentation.Language, manifest, plan, values)
    if err != nil {
    return err
    }
    return writeMergedInstructions(filepath.Join(component.DocumentationRoot(), "INSTRUCTIONS.md"), content)
    }

-func (o *Orchestrator) writeSystemInstructions(root string) error {

  • content, err := prompts.RenderTemplate("templates/system-instructions.md", o.Config.Documentation.Language, o.Config.System.ID)
    +func (o *Orchestrator) writeSystemInstructions(root string, plan model.DocumentationPlan) error {
  • content, err := prompts.RenderSystemInstructions(o.Config.Documentation.Language, o.Config.System.ID, plan)
    if err != nil {
    return err
    }
    @@ -556,14 +742,18 @@ func mergeMarker(existing, replacement, start, end string) string {
    return strings.TrimRight(existing, " \t\r\n") + "\n\n" + replacement + "\n"
    }

-func (o *Orchestrator) prepareSystemWorkspace(root string, components []config.ComponentConfig) error {
+func (o *Orchestrator) prepareSystemWorkspace(root string, components []config.ComponentConfig) (model.DocumentationPlan, error) {

  • systemPlan, err := o.prepareSystemAdaptivePlan(components, true)
  • if err != nil {
  •   return model.DocumentationPlan{}, err
    
  • }
    sourcesRoot := filepath.Join(root, "sources")
    componentsRoot := filepath.Join(sourcesRoot, "components")
    if err := os.RemoveAll(componentsRoot); err != nil {
  •   return err
    
  •   return model.DocumentationPlan{}, err
    
    }
    if err := os.MkdirAll(componentsRoot, 0o755); err != nil {
  •   return err
    
  •   return model.DocumentationPlan{}, err
    

    }

    type manifestComponent struct {
    @@ -577,6 +767,8 @@ func (o *Orchestrator) prepareSystemWorkspace(root string, components []config.C
    Scope string json:"scope,omitempty"
    GitHead string json:"gitHead,omitempty"
    Documentation string json:"documentation"

  •   Discovery        string   `json:"discovery,omitempty"`
    
  •   Plan             string   `json:"plan,omitempty"`
    

    }
    manifest := struct {
    SchemaVersion int json:"schemaVersion"
    @@ -590,9 +782,23 @@ func (o *Orchestrator) prepareSystemWorkspace(root string, components []config.C
    if !fileExists(src) {
    continue
    }

  •   dst := filepath.Join(componentsRoot, component.ID, "openwiki")
    
  •   componentRoot := filepath.Join(componentsRoot, component.ID)
    
  •   dst := filepath.Join(componentRoot, "openwiki")
      if err := copyDir(src, dst); err != nil {
    
  •   	return err
    
  •   	return model.DocumentationPlan{}, err
    
  •   }
    
  •   adaptiveRoot := filepath.Join(o.Config.Workspace, ".wikiforge", "components", component.ID)
    
  •   for _, name := range []string{"discovery.json", "plan.json"} {
    
  •   	source := filepath.Join(adaptiveRoot, name)
    
  •   	if fileExists(source) {
    
  •   		data, err := os.ReadFile(source)
    
  •   		if err != nil {
    
  •   			return model.DocumentationPlan{}, err
    
  •   		}
    
  •   		if err := os.WriteFile(filepath.Join(componentRoot, name), data, 0o644); err != nil {
    
  •   			return model.DocumentationPlan{}, err
    
  •   		}
    
  •   	}
    
  •   }
      manifest.Components = append(manifest.Components, manifestComponent{
      	ID: component.ID, Type: component.Type, Profile: component.Profile,
    

Copy link
Copy Markdown
Owner Author

WIKIFORGE_PHASE1_PATCH_PART 12/19
@@ -600,27 +806,35 @@ func (o *Orchestrator) prepareSystemWorkspace(root string, components []config.C
SourceRepository: component.Repository, Scope: component.Scope,
GitHead: gitHead(component.Repository),
Documentation: "sources/components/" + component.ID + "/openwiki/quickstart.md",

  •   	Discovery:     "sources/components/" + component.ID + "/discovery.json",
    
  •   	Plan:          "sources/components/" + component.ID + "/plan.json",
      })
    
    }
    sort.Slice(manifest.Components, func(i, j int) bool { return manifest.Components[i].ID < manifest.Components[j].ID })
    b, _ := json.MarshalIndent(manifest, "", " ")
    if err := os.WriteFile(filepath.Join(sourcesRoot, "manifest.json"), b, 0o644); err != nil {
  •   return err
    
  •   return model.DocumentationPlan{}, err
    
  • }

  • if err := writeJSON(filepath.Join(sourcesRoot, "system-plan.json"), systemPlan); err != nil {

  •   return model.DocumentationPlan{}, err
    

    }

    if o.Config.System.FactsPath != "" && fileExists(o.Config.System.FactsPath) {
    dst := filepath.Join(root, "facts")
    if filepath.Clean(o.Config.System.FactsPath) != filepath.Clean(dst) {
    if err := os.RemoveAll(dst); err != nil {

  •   		return err
    
  •   		return model.DocumentationPlan{}, err
      	}
      	if err := copyDir(o.Config.System.FactsPath, dst); err != nil {
    
  •   		return err
    
  •   		return model.DocumentationPlan{}, err
      	}
      }
    
    }
  • readme := "# WikiForge System Aggregation Workspace\n\nThe sources/components/ directory contains immutable snapshots of generated component wikis. Components can be applications, modules, libraries, frameworks, contracts, infrastructure, or configuration. OpenWiki must synthesize the whole-system wiki under openwiki/ and must not modify source snapshots.\n"
  • return os.WriteFile(filepath.Join(root, "README.md"), []byte(readme), 0o644)
  • readme := "# WikiForge System Aggregation Workspace\n\nThe sources/components/ directory contains immutable snapshots of generated component wikis plus their deterministic discovery manifests and adaptive plans. Components can be applications, modules, libraries, frameworks, contracts, infrastructure, or configuration. OpenWiki must synthesize the whole-system wiki under openwiki/ and must not modify source snapshots.\n"
  • if err := os.WriteFile(filepath.Join(root, "README.md"), []byte(readme), 0o644); err != nil {
  •   return model.DocumentationPlan{}, err
    
  • }
  • return systemPlan, nil
    }

func (o *Orchestrator) getComponentTarget(st *model.RunState, id string) model.TargetState {
@@ -827,8 +1041,8 @@ func migrateState(st *model.RunState) {
}
}
st.Services = nil

  • if st.Version < 2 {
  •   st.Version = 2
    
  • if st.Version < 3 {
  •   st.Version = 3
    
    }
    }

diff --git a/internal/orchestrator/orchestrator_test.go b/internal/orchestrator/orchestrator_test.go
index 1bdd28617f0d543e45382d212f069bc61d1b6197..5f9074bd62c63369a627463d8792d23416c11aa0 100644
--- a/internal/orchestrator/orchestrator_test.go
+++ b/internal/orchestrator/orchestrator_test.go
@@ -165,12 +165,32 @@ func TestGenerateAllProfilesMonorepoAndSystemEndToEnd(t *testing.T) {
for _, path := range []string{
filepath.Join(root, ".wikiforge", "graph", "app", "nodes.jsonl"),
filepath.Join(root, ".wikiforge", "graph", "system", "edges.jsonl"),

  •   filepath.Join(root, ".wikiforge", "components", "app", "discovery.json"),
    
  •   filepath.Join(root, ".wikiforge", "components", "app", "plan.json"),
    
  •   filepath.Join(root, ".wikiforge", "system", "plan.json"),
    
  •   filepath.Join(cfg.System.Output, "sources", "system-plan.json"),
    
  •   filepath.Join(cfg.System.Output, "sources", "components", "app", "discovery.json"),
    
  •   filepath.Join(cfg.System.Output, "sources", "components", "app", "plan.json"),
      filepath.Join(cfg.System.Output, "openwiki", "system", "infrastructure-deployment.md"),
    

    } {
    if _, err := os.Stat(path); err != nil {
    t.Fatalf("missing %s: %v", path, err)
    }
    }

  • instructions, err := os.ReadFile(filepath.Join(components[0].DocumentationRoot(), "INSTRUCTIONS.md"))

  • if err != nil {

  •   t.Fatal(err)
    
  • }

  • if !strings.Contains(string(instructions), "Adaptive planning context") || !strings.Contains(string(instructions), "api") || !strings.Contains(string(instructions), ".wikiforge/components/app/plan.json") {

  •   t.Fatalf("instructions missing adaptive context or artifact path:\n%s", instructions)
    
  • }

  • systemInstructions, err := os.ReadFile(filepath.Join(cfg.System.Output, "openwiki", "INSTRUCTIONS.md"))

  • if err != nil {

  •   t.Fatal(err)
    
  • }

  • if !strings.Contains(string(systemInstructions), "Adaptive system plan") || !strings.Contains(string(systemInstructions), "system/component-landscape.md") {

  •   t.Fatalf("system instructions missing adaptive plan:\n%s", systemInstructions)
    
  • }

    fr.mu.Lock()
    callsBeforeUpdate := fr.calls
    @@ -319,3 +339,99 @@ func TestRemoveObsoleteDocumentation(t *testing.T) {
    t.Fatal("unrelated canonical documentation was removed")
    }
    }

+type alwaysFailRunner struct{}
+
+func (alwaysFailRunner) Check(context.Context) error { return nil }
+func (alwaysFailRunner) Run(context.Context, string, string, string) (string, error) {

  • return "", fmt.Errorf("intentional generation failure")
    +}

+func TestFailedRunPreservesLastSuccessfulAdaptiveCheckpoint(t *testing.T) {

  • root := t.TempDir()
  • repo := filepath.Join(root, "repo")
  • if err := os.MkdirAll(repo, 0o755); err != nil {
  •   t.Fatal(err)
    
  • }
  • gitInit(t, repo)
  • if err := os.WriteFile(filepath.Join(repo, "README.md"), []byte("# app\n"), 0o644); err != nil {
  •   t.Fatal(err)
    
  • }
  • git(t, repo, "add", ".")
  • git(t, repo, "commit", "-m", "fixture")
  • cfg := config.Defaults()
  • cfg.Workspace = root
  • cfg.Components = []config.ComponentConfig{{ID: "app", Type: "microservice", Profile: "application", Repository: repo, Enabled: true}}
  • cfg.System.Enabled = false
  • cfg.Mermaid.Mode = "basic"
  • cfg.Execution.MaxProcessRetries = 0
  • o := New(cfg, alwaysFailRunner{}, io.Discard)
  • previous := model.TargetState{
  •   Status: "completed", SourceHash: "last-source", DocsHash: "last-docs",
    
  •   DiscoveryHash: "last-discovery", PlanHash: "last-plan", Phases: map[string]model.PhaseStatus{},
    
  • }
  • state := model.RunState{Version: 3, RunID: "previous-success", Components: map[string]model.TargetState{"app": previous}}
  • if err := o.Store.Save(state); err != nil {
  •   t.Fatal(err)
    
  • }
  • if _, err := o.Generate(context.Background(), GenerateOptions{ComponentID: "app", SkipSystem: true}); err == nil {
  •   t.Fatal("expected generation failure")
    
  • }
  • stored, err := o.Store.Load()
  • if err != nil {
  •   t.Fatal(err)
    
  • }
  • got := stored.Components["app"]
  • if got.SourceHash != previous.SourceHash || got.DocsHash != previous.DocsHash || got.DiscoveryHash != previous.DiscoveryHash || got.PlanHash != previous.PlanHash {
  •   t.Fatalf("failed run replaced last-successful checkpoint: got=%+v want=%+v", got, previous)
    
  • }
    +}

+func TestPlanSystemAggregationRespectsIncludeInSystem(t *testing.T) {

  • workspace := t.TempDir()
  • includedRoot := filepath.Join(workspace, "included")
  • excludedRoot := filepath.Join(workspace, "excluded")
  • for _, root := range []string{includedRoot, excludedRoot} {
  •   if err := os.MkdirAll(root, 0o755); err != nil {
    
  •   	t.Fatal(err)
    
  •   }
    
  • }
  • if err := os.WriteFile(filepath.Join(includedRoot, "README.md"), []byte("domain orders"), 0o644); err != nil {
  •   t.Fatal(err)
    
  • }
  • if err := os.WriteFile(filepath.Join(excludedRoot, "README.md"), []byte("domain billing"), 0o644); err != nil {
  •   t.Fatal(err)
    
  • }
  • included := true
  • excluded := false
  • cfg := config.Defaults()
  • cfg.Workspace = workspace
  • cfg.Components = []config.ComponentConfig{
  •   {ID: "included", Type: "microservice", Profile: "application", Repository: includedRoot, Enabled: true, IncludeInSystem: &included, Capabilities: []string{"orders"}},
    
  •   {ID: "excluded", Type: "microservice", Profile: "application", Repository: excludedRoot, Enabled: true, IncludeInSystem: &excluded, Capabilities: []string{"billing"}},
    
  • }
  • o := New(cfg, &fakeRunner{}, io.Discard)
  • if _, err := o.PlanWithExplain("", true, false); err != nil {
  •   t.Fatal(err)
    
  • }
  • data, err := os.ReadFile(filepath.Join(workspace, ".wikiforge", "system", "plan.json"))
  • if err != nil {
  •   t.Fatal(err)
    
  • }
  • var plan model.DocumentationPlan
  • if err := json.Unmarshal(data, &plan); err != nil {
  •   t.Fatal(err)
    
  • }
  • for _, unit := range plan.Units {
  •   if unit.ComponentID == "excluded" {
    
  •   	t.Fatalf("excluded component leaked into system plan: %+v", plan.Units)
    
  •   }
    
  • }
  • foundIncluded := false
  • for _, unit := range plan.Units {
  •   foundIncluded = foundIncluded || unit.ComponentID == "included"
    
  • }
  • if !foundIncluded {
  •   t.Fatalf("included component missing from system plan: %+v", plan.Units)
    
  • }
    +}
    diff --git a/internal/planner/integration_test.go b/internal/planner/integration_test.go
    new file mode 100644
    index 0000000000000000000000000000000000000000..f7af45c25184063df57dc661961ef10ff5878f1a
    --- /dev/null
    +++ b/internal/planner/integration_test.go
    @@ -0,0 +1,79 @@
    +package planner_test

+import (

  • "os"
  • "path/filepath"
  • "testing"
  • "github.com/example/wikiforge/internal/config"
  • "github.com/example/wikiforge/internal/discovery"
  • "github.com/example/wikiforge/internal/model"
  • "github.com/example/wikiforge/internal/planner"
    +)

+func TestRepresentativeRepositoryTypesProduceRelevantAdaptivePlans(t *testing.T) {

  • cases := []struct {
  •   name, componentType, profile, rel, content, expectedPack, expectedUnit string
    
  • }{
  •   {"monolith", "monolith", "application", "db/schema.sql", "create table orders(id bigint);", "database", ""},
    
  •   {"modular", "modular-monolith", "modular-application", "modules/orders/README.md", "bounded context and business rules", "domain", "orders"},
    
  •   {"microservice", "microservice", "application", "api/openapi.yaml", "openapi: 3.1.0", "api", ""},
    
  •   {"library", "library", "reusable", "src/pool.go", "mutex semaphore thread safe", "concurrency", ""},
    
  •   {"framework", "framework", "reusable", "src/telemetry.go", "opentelemetry tracing metrics", "telemetry", ""},
    
  •   {"infrastructure", "iac", "infrastructure", "deploy/deployment.yaml", "kind: Deployment kubernetes", "container-runtime", ""},
    
  • }
  • for _, tc := range cases {
  •   t.Run(tc.name, func(t *testing.T) {
    
  •   	root := t.TempDir()
    
  •   	path := filepath.Join(root, filepath.FromSlash(tc.rel))
    
  •   	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
    
  •   		t.Fatal(err)
    
  •   	}
    
  •   	if err := os.WriteFile(path, []byte(tc.content), 0o644); err != nil {
    
  •   		t.Fatal(err)
    
  •   	}
    
  •   	cfg := config.Defaults()
    
  •   	component := config.ComponentConfig{ID: tc.name, Type: tc.componentType, Profile: tc.profile, Repository: root, Enabled: true}
    
  •   	cfg.Components = []config.ComponentConfig{component}
    
  •   	manifest, err := discovery.Discover(cfg, component)
    
  •   	if err != nil {
    
  •   		t.Fatal(err)
    
  •   	}
    
  •   	plan := planner.Build(cfg, component, manifest)
    
  •   	if !contains(plan.SelectedPacks, tc.expectedPack) {
    
  •   		t.Fatalf("packs=%v want %s", plan.SelectedPacks, tc.expectedPack)
    
  •   	}
    
  •   	if !pageForPack(plan, tc.expectedPack) {
    
  •   		t.Fatalf("pack %s has no page: %+v", tc.expectedPack, plan.Pages)
    
  •   	}
    
  •   	if tc.expectedUnit != "" && !unitExists(plan.Units, tc.expectedUnit) {
    
  •   		t.Fatalf("units=%+v want %s", plan.Units, tc.expectedUnit)
    
  •   	}
    
  •   })
    
  • }
    +}

+func contains(values []string, wanted string) bool {

  • for _, value := range values {
  •   if value == wanted {
    
  •   	return true
    
  •   }
    
  • }
  • return false
    +}
    +func pageForPack(plan model.DocumentationPlan, pack string) bool {
  • for _, page := range plan.Pages {
  •   if page.Pack == pack {
    
  •   	return true
    
  •   }
    
  • }
  • return false
    +}
    +func unitExists(units []model.DocumentationUnit, id string) bool {
  • for _, unit := range units {
  •   if unit.ID == id {
    
  •   	return true
    
  •   }
    
  • }
  • return false
    +}
    diff --git a/internal/planner/planner.go b/internal/planner/planner.go
    new file mode 100644
    index 0000000000000000000000000000000000000000..743343c2c4c92b0206ea085ac73212709b7dd59b
    --- /dev/null

Copy link
Copy Markdown
Owner Author

WIKIFORGE_PHASE1_PATCH_PART 13/19
+++ b/internal/planner/planner.go
@@ -0,0 +1,360 @@
+package planner
+
+import (

  • "fmt"
  • "sort"
  • "strings"
  • "github.com/example/wikiforge/internal/config"
  • "github.com/example/wikiforge/internal/model"
    +)

+type packPage struct{ path, view, kind string }
+
+var packPages = map[string]packPage{

  • "api": {"catalogs/interfaces/index.md", "catalog", "collection"},
  • "cache": {"platform/caching.md", "platform", "single"},
  • "concurrency": {"platform/concurrency-and-context.md", "platform", "single"},
  • "configuration": {"catalogs/configuration/index.md", "catalog", "collection"},
  • "container-runtime": {"platform/containerization.md", "platform", "single"},
  • "cryptography": {"platform/cryptography.md", "platform", "single"},
  • "data-access": {"catalogs/data-access/index.md", "catalog", "collection"},
  • "database": {"catalogs/data/index.md", "catalog", "collection"},
  • "distributed-coordination": {"platform/distributed-coordination.md", "platform", "single"},
  • "domain": {"domains/index.md", "domain", "index"},
  • "engineering": {"engineering/standards.md", "engineering", "single"},
  • "files": {"catalogs/files/index.md", "catalog", "collection"},
  • "jobs": {"catalogs/jobs/index.md", "catalog", "collection"},
  • "messaging": {"catalogs/events/index.md", "catalog", "collection"},
  • "migrations": {"catalogs/migrations/index.md", "catalog", "collection"},
  • "rate-limit": {"platform/rate-limiting.md", "platform", "single"},
  • "runtime": {"runtime-and-operations.md", "component", "single"},
  • "security": {"platform/security-and-identity.md", "platform", "single"},
  • "telemetry": {"platform/telemetry.md", "platform", "single"},
  • "workflow": {"flows/index.md", "flow", "index"},
    +}

+func Build(cfg config.Config, component config.ComponentConfig, manifest model.DiscoveryManifest) model.DocumentationPlan {

  • plan := model.DocumentationPlan{
  •   SchemaVersion:      1,
    
  •   ComponentID:        component.ID,
    
  •   Profile:            component.Profile,
    
  •   Views:              append([]string(nil), cfg.Documentation.Views...),
    
  •   Units:              append([]model.DocumentationUnit(nil), manifest.Units...),
    
  •   ShardBy:            append([]string(nil), cfg.Documentation.Catalogs.ShardBy...),
    
  •   MaximumRowsPerPage: cfg.Documentation.Catalogs.MaximumRowsPerPage,
    
  • }
  • selected := map[string]bool{}
  • for _, pack := range config.DefaultPacksForProfile(component.Profile) {
  •   selected[pack] = true
    
  • }
  • for _, pack := range component.Packs {
  •   selected[pack] = true
    
  • }
  • for _, pack := range manifest.Packs {
  •   selected[pack] = true
    
  • }
  • plan.SelectedPacks = mapKeys(selected)
  • pagePaths := map[string]model.PlanPage{}
  • add := func(page model.PlanPage) bool {
  •   if page.Path == "" {
    
  •   	return false
    
  •   }
    
  •   if existing, found := pagePaths[page.Path]; found {
    
  •   	if existing.ID != page.ID {
    
  •   		plan.Decisions = append(plan.Decisions, model.PlanDecision{Subject: page.ID, Action: "defer", Reason: fmt.Sprintf("Output path %s conflicts with planned page %s.", page.Path, existing.ID)})
    
  •   	}
    
  •   	return false
    
  •   }
    
  •   pagePaths[page.Path] = page
    
  •   plan.Pages = append(plan.Pages, page)
    
  •   return true
    
  • }
  • add(model.PlanPage{ID: component.ID + ":quickstart", Path: "quickstart.md", View: "component", Kind: "index", Reason: "Every component requires one bounded entry point, independent of optional detailed views."})
  • if cfg.ViewEnabled("component") {
  •   add(model.PlanPage{ID: component.ID + ":overview", Path: "components/" + component.ID + "/index.md", View: "component", Kind: "index", Reason: "Component view is enabled."})
    
  • }
  • for _, pack := range plan.SelectedPacks {
  •   contract, ok := packPages[pack]
    
  •   if !ok {
    
  •   	plan.Decisions = append(plan.Decisions, model.PlanDecision{Subject: pack, Action: "defer", Reason: "No Phase 1 canonical page mapping is registered."})
    
  •   	continue
    
  •   }
    
  •   if !cfg.ViewEnabled(contract.view) {
    
  •   	plan.Decisions = append(plan.Decisions, model.PlanDecision{Subject: pack, Action: "defer", Reason: fmt.Sprintf("Required %s view is disabled.", contract.view)})
    
  •   	continue
    
  •   }
    
  •   path := contract.path
    
  •   if pack == "runtime" {
    
  •   	path = "components/" + component.ID + "/" + path
    
  •   }
    
  •   page := model.PlanPage{ID: component.ID + ":pack:" + pack, Path: path, View: contract.view, Pack: pack, Kind: contract.kind, Reason: packReason(component, manifest, pack)}
    
  •   if contract.kind == "collection" {
    
  •   	page.ShardBy = append([]string(nil), plan.ShardBy...)
    
  •   	page.MaximumRowsPerPage = plan.MaximumRowsPerPage
    
  •   }
    
  •   if add(page) {
    
  •   	plan.Decisions = append(plan.Decisions, model.PlanDecision{Subject: pack, Action: "include", Reason: packReason(component, manifest, pack)})
    
  •   }
    
  • }
  • for _, unit := range plan.Units {
  •   view := viewForUnit(unit.Kind)
    
  •   if !cfg.ViewEnabled(view) {
    
  •   	plan.Decisions = append(plan.Decisions, model.PlanDecision{Subject: unit.ID, Action: "defer", Reason: fmt.Sprintf("Documentation unit requires disabled %s view.", view)})
    
  •   	continue
    
  •   }
    
  •   path := unitPagePath(unit)
    
  •   add(model.PlanPage{ID: component.ID + ":unit:" + unit.ID, Path: path, View: view, UnitID: unit.ID, Kind: "unit", Reason: fmt.Sprintf("%s documentation unit (%s).", unit.Origin, unit.Kind)})
    
  • }
  • // Materialize hierarchical view indexes after leaf selection so navigation can
  • // remain shallow without forcing empty areas.
  • viewChildren := map[string]bool{}
  • for _, page := range plan.Pages {
  •   viewChildren[page.View] = true
    
  • }
  • if cfg.ViewEnabled("component") && viewChildren["component"] {
  •   add(model.PlanPage{ID: component.ID + ":view:components", Path: "components/index.md", View: "component", Kind: "index", Reason: "Component view contains planned child pages."})
    
  • }
  • if cfg.ViewEnabled("domain") && (viewChildren["domain"] || hasUnitKind(plan.Units, "domain", "subdomain", "bounded-context")) {
  •   add(model.PlanPage{ID: component.ID + ":view:domains", Path: "domains/index.md", View: "domain", Kind: "index", Reason: "Domain view contains planned units or capability pages."})
    
  • }
  • if cfg.ViewEnabled("flow") && (viewChildren["flow"] || hasUnitKind(plan.Units, "flow")) {
  •   add(model.PlanPage{ID: component.ID + ":view:flows", Path: "flows/index.md", View: "flow", Kind: "index", Reason: "Flow view contains planned units or workflow pages."})
    
  • }
  • if cfg.ViewEnabled("catalog") && viewChildren["catalog"] {
  •   add(model.PlanPage{ID: component.ID + ":view:catalogs", Path: "catalogs/index.md", View: "catalog", Kind: "index", Reason: "Catalog view contains one or more typed collections."})
    
  • }
  • if cfg.ViewEnabled("platform") && viewChildren["platform"] {
  •   add(model.PlanPage{ID: component.ID + ":view:platform", Path: "platform/index.md", View: "platform", Kind: "index", Reason: "Platform view contains shared technical mechanisms."})
    
  • }
  • if cfg.ViewEnabled("engineering") && viewChildren["engineering"] {
  •   add(model.PlanPage{ID: component.ID + ":view:engineering", Path: "engineering/index.md", View: "engineering", Kind: "index", Reason: "Engineering view contains standards or contribution guidance."})
    
  • }
  • if cfg.ViewEnabled("operations") && operational(plan.SelectedPacks) {
  •   add(model.PlanPage{ID: component.ID + ":view:operations", Path: "operations/index.md", View: "operations", Kind: "index", Reason: "Selected runtime capabilities have operational consequences."})
    
  • }
  • for _, pack := range config.SupportedCapabilityPacks() {
  •   if !selected[pack] {
    
  •   	plan.Decisions = append(plan.Decisions, model.PlanDecision{Subject: pack, Action: "skip", Reason: "Neither configured, profile-required, nor discovered from source evidence."})
    
  •   }
    
  • }
  • sort.Slice(plan.Pages, func(i, j int) bool { return plan.Pages[i].Path < plan.Pages[j].Path })
  • sort.Slice(plan.Decisions, func(i, j int) bool {
  •   if plan.Decisions[i].Subject == plan.Decisions[j].Subject {
    
  •   	return plan.Decisions[i].Action < plan.Decisions[j].Action
    
  •   }
    
  •   return plan.Decisions[i].Subject < plan.Decisions[j].Subject
    
  • })
  • return plan
    +}

+func Explain(plan model.DocumentationPlan) []string {

  • lines := []string{fmt.Sprintf("adaptive plan component=%s profile=%s packs=%d units=%d pages=%d", plan.ComponentID, plan.Profile, len(plan.SelectedPacks), len(plan.Units), len(plan.Pages))}
  • if len(plan.SelectedPacks) > 0 {
  •   lines = append(lines, "  packs: "+strings.Join(plan.SelectedPacks, ", "))
    
  • }
  • for _, page := range plan.Pages {
  •   policy := ""
    
  •   if len(page.ShardBy) > 0 {
    
  •   	policy = fmt.Sprintf(" shardBy=%s maxRows=%d", strings.Join(page.ShardBy, ","), page.MaximumRowsPerPage)
    
  •   }
    
  •   lines = append(lines, fmt.Sprintf("  include %-45s view=%-11s kind=%-10s%s reason=%s", page.Path, page.View, page.Kind, policy, page.Reason))
    
  • }
  • for _, decision := range plan.Decisions {
  •   if decision.Action != "include" {
    
  •   	lines = append(lines, fmt.Sprintf("  %-7s %-24s %s", decision.Action, decision.Subject, decision.Reason))
    
  •   }
    
  • }
  • return lines
    +}

+func packReason(component config.ComponentConfig, manifest model.DiscoveryManifest, pack string) string {

  • for _, explicit := range component.Packs {
  •   if explicit == pack {
    
  •   	return "Explicitly configured capability pack."
    
  •   }
    
  • }
  • for _, evidence := range manifest.Evidence {
  •   if evidence.Pack == pack {
    
  •   	return fmt.Sprintf("Discovered from %d evidence file(s).", evidence.Count)
    
  •   }
    
  • }
  • for _, candidate := range config.DefaultPacksForProfile(component.Profile) {
  •   if candidate == pack {
    
  •   	return "Required by the base documentation profile."
    
  •   }
    
  • }
  • return "Selected by normalized planning inputs."
    +}

+func viewForUnit(kind string) string {

  • switch kind {
  • case "flow":
  •   return "flow"
    
  • case "platform":
  •   return "platform"
    
  • case "catalog":
  •   return "catalog"
    
  • case "component", "module":
  •   return "component"
    
  • default:
  •   return "domain"
    
  • }
    +}
    +func unitPagePath(unit model.DocumentationUnit) string {
  • path := strings.TrimSuffix(unit.OutputPath, "/")
  • if path == "" {
  •   path = defaultUnitOutput(unit)
    
  • }
  • if unit.Kind == "flow" {
  •   if !strings.HasSuffix(path, ".md") {
    
  •   	path += ".md"
    
  •   }
    
  •   return path
    
  • }
  • if strings.HasSuffix(path, ".md") {
  •   return path
    
  • }
  • return path + "/index.md"
    +}

+func defaultUnitOutput(unit model.DocumentationUnit) string {

  • switch unit.Kind {
  • case "flow":
  •   return "flows/" + unit.ID
    
  • case "module":
  •   return "components/" + unit.ComponentID + "/modules/" + unit.ID
    
  • case "component":
  •   return "components/" + unit.ID
    
  • case "platform":
  •   return "platform/" + unit.ID
    
  • case "catalog":
  •   return "catalogs/" + unit.ID
    
  • default:
  •   return "domains/" + unit.ID
    
  • }
    +}
    +func hasUnitKind(units []model.DocumentationUnit, kinds ...string) bool {
  • wanted := map[string]bool{}
  • for _, kind := range kinds {
  •   wanted[kind] = true
    
  • }
  • for _, unit := range units {
  •   if wanted[unit.Kind] {
    
  •   	return true
    
  •   }
    
  • }
  • return false
    +}

+func operational(packs []string) bool {

  • wanted := map[string]bool{"container-runtime": true, "jobs": true, "messaging": true, "runtime": true, "telemetry": true, "workflow": true}
  • for _, pack := range packs {
  •   if wanted[pack] {
    
  •   	return true
    
  •   }
    
  • }
  • return false
    +}

+func containsPack(packs []string, wanted string) bool {

  • for _, pack := range packs {
  •   if pack == wanted {
    
  •   	return true
    
  •   }
    
  • }
  • return false
    +}

+func anyPackInView(packs []string, view string) bool {

  • for _, pack := range packs {
  •   if page, ok := packPages[pack]; ok && page.view == view {
    
  •   	return true
    
  •   }
    
  • }
  • return false
    +}

+func mapKeys(values map[string]bool) []string {

  • out := make([]string, 0, len(values))
  • for value := range values {
  •   out = append(out, value)
    
  • }
  • sort.Strings(out)
  • return out
    +}

Copy link
Copy Markdown
Owner Author

WIKIFORGE_PHASE1_PATCH_PART 14/19
+func BuildSystem(cfg config.Config, componentPlans []model.DocumentationPlan) model.DocumentationPlan {

  • plan := model.DocumentationPlan{
  •   SchemaVersion:      1,
    
  •   ComponentID:        cfg.System.ID,
    
  •   Profile:            "system",
    
  •   Views:              append([]string(nil), cfg.Documentation.Views...),
    
  •   ShardBy:            append([]string(nil), cfg.Documentation.Catalogs.ShardBy...),
    
  •   MaximumRowsPerPage: cfg.Documentation.Catalogs.MaximumRowsPerPage,
    
  • }
  • packs := map[string]bool{}
  • units := map[string]model.DocumentationUnit{}
  • for _, componentPlan := range componentPlans {
  •   for _, pack := range componentPlan.SelectedPacks {
    
  •   	packs[pack] = true
    
  •   }
    
  •   for _, unit := range componentPlan.Units {
    
  •   	units[unit.ComponentID+":"+unit.ID] = unit
    
  •   }
    
  • }
  • plan.SelectedPacks = mapKeys(packs)
  • for _, unit := range units {
  •   plan.Units = append(plan.Units, unit)
    
  • }
  • sort.Slice(plan.Units, func(i, j int) bool {
  •   if plan.Units[i].ComponentID == plan.Units[j].ComponentID {
    
  •   	return plan.Units[i].ID < plan.Units[j].ID
    
  •   }
    
  •   return plan.Units[i].ComponentID < plan.Units[j].ComponentID
    
  • })
  • if !cfg.ViewEnabled("system") {
  •   plan.Decisions = append(plan.Decisions, model.PlanDecision{Subject: "system", Action: "defer", Reason: "System view is disabled."})
    
  •   return plan
    
  • }
  • pages := []model.PlanPage{
  •   {ID: cfg.System.ID + ":quickstart", Path: "quickstart.md", View: "system", Kind: "index", Reason: "Whole-system documentation requires one bounded entry point."},
    
  •   {ID: cfg.System.ID + ":index", Path: "system/index.md", View: "system", Kind: "index", Reason: "System view is enabled."},
    
  •   {ID: cfg.System.ID + ":overview", Path: "system/overview.md", View: "system", Kind: "single", Reason: "Provides the system boundary and synthesis context."},
    
  •   {ID: cfg.System.ID + ":components", Path: "system/component-landscape.md", View: "system", Kind: "single", Reason: "Component plans are available for aggregation."},
    
  •   {ID: cfg.System.ID + ":runtime", Path: "system/runtime-topology.md", View: "system", Kind: "single", Reason: "Runtime and dependency packs require a system topology view."},
    
  • }
  • if len(plan.Units) > 0 {
  •   pages = append(pages,
    
  •   	model.PlanPage{ID: cfg.System.ID + ":capabilities", Path: "system/capability-map.md", View: "system", Kind: "single", Reason: "Documentation units expose capabilities across components."},
    
  •   	model.PlanPage{ID: cfg.System.ID + ":domains", Path: "system/domain-map.md", View: "system", Kind: "single", Reason: "Domain and flow units require cross-component mapping."},
    
  •   )
    
  • }
  • if cfg.ViewEnabled("component") {
  •   pages = append(pages, model.PlanPage{ID: cfg.System.ID + ":view:components", Path: "components/index.md", View: "component", Kind: "index", Reason: "Aggregated component plans are available."})
    
  • }
  • if cfg.ViewEnabled("domain") && hasUnitKind(plan.Units, "domain", "subdomain", "bounded-context") {
  •   pages = append(pages, model.PlanPage{ID: cfg.System.ID + ":view:domains", Path: "domains/index.md", View: "domain", Kind: "index", Reason: "Aggregated domain units are available."})
    
  • }
  • if cfg.ViewEnabled("flow") && (hasUnitKind(plan.Units, "flow") || containsPack(plan.SelectedPacks, "workflow")) {
  •   pages = append(pages, model.PlanPage{ID: cfg.System.ID + ":view:flows", Path: "flows/index.md", View: "flow", Kind: "index", Reason: "Aggregated flow evidence is available."})
    
  • }
  • if cfg.ViewEnabled("catalog") && anyPackInView(plan.SelectedPacks, "catalog") {
  •   pages = append(pages, model.PlanPage{ID: cfg.System.ID + ":view:catalogs", Path: "catalogs/index.md", View: "catalog", Kind: "index", Reason: "Aggregated typed catalogs are required."})
    
  • }
  • if cfg.ViewEnabled("platform") && anyPackInView(plan.SelectedPacks, "platform") {
  •   pages = append(pages, model.PlanPage{ID: cfg.System.ID + ":view:platform", Path: "platform/index.md", View: "platform", Kind: "index", Reason: "Aggregated platform mechanisms are required."})
    
  • }
  • if cfg.ViewEnabled("engineering") && containsPack(plan.SelectedPacks, "engineering") {
  •   pages = append(pages, model.PlanPage{ID: cfg.System.ID + ":view:engineering", Path: "engineering/index.md", View: "engineering", Kind: "index", Reason: "Aggregated engineering guidance is available."})
    
  • }
  • if cfg.ViewEnabled("operations") && operational(plan.SelectedPacks) {
  •   pages = append(pages, model.PlanPage{ID: cfg.System.ID + ":view:operations", Path: "operations/index.md", View: "operations", Kind: "index", Reason: "Aggregated runtime capabilities have operational consequences."})
    
  • }
  • plan.Pages = pages
  • plan.Decisions = append(plan.Decisions, model.PlanDecision{Subject: "system", Action: "include", Reason: fmt.Sprintf("Aggregates %d component plan(s).", len(componentPlans))})
  • sort.Slice(plan.Pages, func(i, j int) bool { return plan.Pages[i].Path < plan.Pages[j].Path })
  • return plan
    +}
    diff --git a/internal/planner/planner_test.go b/internal/planner/planner_test.go
    new file mode 100644
    index 0000000000000000000000000000000000000000..bbcc3f670da4bdddd114ba673f6c484dfc9bf743
    --- /dev/null
    +++ b/internal/planner/planner_test.go
    @@ -0,0 +1,171 @@
    +package planner

+import (

  • "testing"
  • "github.com/example/wikiforge/internal/config"
  • "github.com/example/wikiforge/internal/model"
    +)

+func TestEverySupportedPackHasCanonicalPlanningOutcome(t *testing.T) {

  • cfg := config.Defaults()
  • component := config.ComponentConfig{ID: "app", Type: "microservice", Profile: "application", Repository: t.TempDir(), Enabled: true, Packs: config.SupportedCapabilityPacks()}
  • manifest := model.DiscoveryManifest{Component: model.Component{ID: component.ID}, Packs: config.SupportedCapabilityPacks()}
  • plan := Build(cfg, component, manifest)
  • for _, pack := range config.SupportedCapabilityPacks() {
  •   if !hasPackPage(plan, pack) {
    
  •   	t.Errorf("pack %s has no canonical page: %+v", pack, plan.Decisions)
    
  •   }
    
  •   if hasDecision(plan, pack, "defer") {
    
  •   	t.Errorf("pack %s was unexpectedly deferred", pack)
    
  •   }
    
  • }
  • if len(plan.Pages) != len(uniquePaths(plan.Pages)) {
  •   t.Fatal("planner emitted duplicate page paths")
    
  • }
    +}

+func TestPlannerDefersPackWhenRequiredViewDisabled(t *testing.T) {

  • cfg := config.Defaults()
  • cfg.Documentation.Views = []string{"component", "catalog"}
  • component := config.ComponentConfig{ID: "app", Type: "microservice", Profile: "application", Repository: t.TempDir(), Enabled: true, Packs: []string{"cache"}}
  • plan := Build(cfg, component, model.DiscoveryManifest{Packs: []string{"cache"}})
  • if !hasDecision(plan, "cache", "defer") {
  •   t.Fatalf("expected cache deferral: %+v", plan.Decisions)
    
  • }
  • if hasPackPage(plan, "cache") {
  •   t.Fatal("cache page should not be planned with platform view disabled")
    
  • }
    +}

+func TestProfilesProduceDifferentComposablePlans(t *testing.T) {

  • cfg := config.Defaults()
  • app := config.ComponentConfig{ID: "app", Type: "microservice", Profile: "application", Enabled: true}
  • lib := config.ComponentConfig{ID: "lib", Type: "framework", Profile: "reusable", Enabled: true}
  • infra := config.ComponentConfig{ID: "infra", Type: "iac", Profile: "infrastructure", Enabled: true}
  • appPlan := Build(cfg, app, model.DiscoveryManifest{})
  • libPlan := Build(cfg, lib, model.DiscoveryManifest{})
  • infraPlan := Build(cfg, infra, model.DiscoveryManifest{})
  • if equalStrings(appPlan.SelectedPacks, libPlan.SelectedPacks) || equalStrings(appPlan.SelectedPacks, infraPlan.SelectedPacks) || equalStrings(libPlan.SelectedPacks, infraPlan.SelectedPacks) {
  •   t.Fatalf("profiles should produce different packs: app=%v lib=%v infra=%v", appPlan.SelectedPacks, libPlan.SelectedPacks, infraPlan.SelectedPacks)
    
  • }
    +}

+func TestDocumentationUnitsAndShardPolicyArePreserved(t *testing.T) {

  • cfg := config.Defaults()
  • cfg.Documentation.Catalogs.ShardBy = []string{"owner", "domain"}
  • cfg.Documentation.Catalogs.MaximumRowsPerPage = 42
  • component := config.ComponentConfig{ID: "app", Type: "microservice", Profile: "application", Enabled: true}
  • manifest := model.DiscoveryManifest{Units: []model.DocumentationUnit{{ID: "submit-order", ComponentID: "app", Kind: "flow", OutputPath: "flows/submit-order", Origin: "configured"}}}
  • plan := Build(cfg, component, manifest)
  • if plan.MaximumRowsPerPage != 42 || len(plan.ShardBy) != 2 {
  •   t.Fatalf("shard policy lost: %+v", plan)
    
  • }
  • if !hasPath(plan, "flows/submit-order.md") {
  •   t.Fatalf("flow page missing: %+v", plan.Pages)
    
  • }
  • page, ok := pageByPath(plan, "catalogs/interfaces/index.md")
  • if !ok || page.Kind != "collection" || page.MaximumRowsPerPage != 42 || len(page.ShardBy) != 2 {
  •   t.Fatalf("collection shard policy was not attached to the planned page: %+v", page)
    
  • }
    +}

+func pageByPath(plan model.DocumentationPlan, path string) (model.PlanPage, bool) {

  • for _, page := range plan.Pages {
  •   if page.Path == path {
    
  •   	return page, true
    
  •   }
    
  • }
  • return model.PlanPage{}, false
    +}

+func hasPackPage(plan model.DocumentationPlan, pack string) bool {

  • for _, page := range plan.Pages {
  •   if page.Pack == pack {
    
  •   	return true
    
  •   }
    
  • }
  • return false
    +}
    +func hasDecision(plan model.DocumentationPlan, subject, action string) bool {
  • for _, decision := range plan.Decisions {
  •   if decision.Subject == subject && decision.Action == action {
    
  •   	return true
    
  •   }
    
  • }
  • return false
    +}
    +func hasPath(plan model.DocumentationPlan, path string) bool {
  • for _, page := range plan.Pages {
  •   if page.Path == path {
    
  •   	return true
    
  •   }
    
  • }
  • return false
    +}
    +func uniquePaths(pages []model.PlanPage) map[string]bool {
  • out := map[string]bool{}
  • for _, page := range pages {
  •   out[page.Path] = true
    
  • }
  • return out
    +}
    +func equalStrings(a, b []string) bool {
  • if len(a) != len(b) {
  •   return false
    
  • }
  • for i := range a {
  •   if a[i] != b[i] {
    
  •   	return false
    
  •   }
    
  • }
  • return true
    +}

+func TestSystemPlanAggregatesComponentPacksAndUnits(t *testing.T) {

  • cfg := config.Defaults()
  • plans := []model.DocumentationPlan{
  •   {ComponentID: "app", SelectedPacks: []string{"api", "messaging"}, Units: []model.DocumentationUnit{{ID: "orders", ComponentID: "app", Kind: "domain"}}},
    
  •   {ComponentID: "worker", SelectedPacks: []string{"jobs", "messaging"}},
    
  • }
  • plan := BuildSystem(cfg, plans)
  • if plan.Profile != "system" || !hasPath(plan, "system/component-landscape.md") || !hasPath(plan, "system/domain-map.md") {
  •   t.Fatalf("system plan=%+v", plan)
    
  • }
  • if len(plan.SelectedPacks) != 3 || len(plan.Units) != 1 {
  •   t.Fatalf("aggregation packs=%v units=%v", plan.SelectedPacks, plan.Units)
    
  • }
    +}

+func TestQuickstartRemainsPlannedWhenDetailedComponentViewDisabled(t *testing.T) {

  • cfg := config.Defaults()
  • cfg.Documentation.Views = []string{"domain"}
  • component := config.ComponentConfig{ID: "app", Type: "microservice", Profile: "application", Enabled: true}
  • plan := Build(cfg, component, model.DiscoveryManifest{})
  • if !hasPath(plan, "quickstart.md") {
  •   t.Fatal("root navigation entry point must not disappear with the detailed component view")
    
  • }
  • if hasPath(plan, "components/app/index.md") {
  •   t.Fatal("detailed component page should respect the disabled component view")
    
  • }
    +}

+func TestPlannerReportsOutputCollisionInsteadOfSilentlyDroppingUnit(t *testing.T) {

  • cfg := config.Defaults()
  • component := config.ComponentConfig{ID: "app", Type: "microservice", Profile: "application", Enabled: true}
  • manifest := model.DiscoveryManifest{Units: []model.DocumentationUnit{{ID: "conflict", ComponentID: "app", Kind: "domain", OutputPath: "domains/index.md", Origin: "configured"}}}
  • plan := Build(cfg, component, manifest)
  • if !hasDecision(plan, "app:unit:conflict", "defer") {
  •   t.Fatalf("expected explicit collision decision: %+v", plan.Decisions)
    
  • }
    +}

+func TestUnitOutputAcceptsExplicitMarkdownPath(t *testing.T) {

  • cfg := config.Defaults()
  • component := config.ComponentConfig{ID: "app", Type: "microservice", Profile: "application", Enabled: true}

Copy link
Copy Markdown
Owner Author

WIKIFORGE_PHASE1_PATCH_PART 15/19

  • manifest := model.DiscoveryManifest{Units: []model.DocumentationUnit{{ID: "orders", ComponentID: "app", Kind: "domain", OutputPath: "domains/orders/overview.md", Origin: "configured"}}}
  • plan := Build(cfg, component, manifest)
  • if !hasPath(plan, "domains/orders/overview.md") {
  •   t.Fatalf("explicit Markdown output was rewritten incorrectly: %+v", plan.Pages)
    
  • }
    +}
    diff --git a/internal/prompts/prompts.go b/internal/prompts/prompts.go
    index 8db2efba0f49d31e7f7b01768fbd542f8ae59d2d..b443057be5283967cde37c36186c98b272b9361d 100644
    --- a/internal/prompts/prompts.go
    +++ b/internal/prompts/prompts.go
    @@ -45,7 +45,8 @@ func RenderComponentPhase(phase model.Phase, profile Profile, component config.C
    if len(phase.PageContracts) > 0 {
    supplemental = phase.PageContracts
    }
  • common := map[string]string{
  • common := AdaptiveValues(model.DiscoveryManifest{}, model.DocumentationPlan{})
  • for key, value := range map[string]string{
    "PROFILE_ID": profile.ID,
    "PROFILE_NAME": profile.DisplayName,
    "PROFILE_DESCRIPTION": profile.Description,
    @@ -60,6 +61,8 @@ func RenderComponentPhase(phase model.Phase, profile Profile, component config.C
    "DIAGRAM_CONTRACT": diagramContract(phase.RequiredDiagram),
    "GUIDANCE": profileGuidance(profile.ID, component.Type),
    "SUPPLEMENTAL_CONTRACTS": supplementalContractsText(supplemental),
  • } {
  •   common[key] = value
    
    }
    for k, v := range values {
    common[k] = v
    @@ -68,14 +71,20 @@ func RenderComponentPhase(phase model.Phase, profile Profile, component config.C
    }

func RenderSystemPhase(phase model.Phase, language, targetID string) (string, error) {

  • return RenderSystemPhaseWithPlan(phase, language, targetID, model.DocumentationPlan{})
    +}

+func RenderSystemPhaseWithPlan(phase model.Phase, language, targetID string, plan model.DocumentationPlan) (string, error) {
supplemental := SystemSupplementalPages
if len(phase.PageContracts) > 0 {
supplemental = phase.PageContracts
}

  • return Render(phase.PromptAsset, language, targetID, map[string]string{
  •   "SYSTEM_CANONICAL_FILES":        systemCanonicalFilesText(),
    
  •   "SYSTEM_SUPPLEMENTAL_CONTRACTS": supplementalContractsText(supplemental),
    
  • })
  • values := AdaptiveValues(model.DiscoveryManifest{}, plan)
  • values["DISCOVERY_ARTIFACT"] = "sources/components/*/discovery.json"
  • values["PLAN_ARTIFACT"] = "sources/system-plan.json"
  • values["SYSTEM_CANONICAL_FILES"] = systemCanonicalFilesText()
  • values["SYSTEM_SUPPLEMENTAL_CONTRACTS"] = supplementalContractsText(supplemental)
  • return Render(phase.PromptAsset, language, targetID, values)
    }

func systemCanonicalFilesText() string {
@@ -87,15 +96,31 @@ func systemCanonicalFilesText() string {
}

func RenderComponentUpdate(profile Profile, component config.ComponentConfig, language string) (string, error) {

  • return RenderComponentPhase(model.Phase{PromptAsset: "prompts/component/update.md"}, profile, component, language, nil)
  • return RenderComponentUpdateWithValues(profile, component, language, nil)
    +}

+func RenderComponentUpdateWithValues(profile Profile, component config.ComponentConfig, language string, values map[string]string) (string, error) {

  • return RenderComponentPhase(model.Phase{PromptAsset: "prompts/component/update.md"}, profile, component, language, values)
    }

func RenderSystemUpdate(language, targetID string) (string, error) {

  • return RenderSystemPhase(model.Phase{PromptAsset: "prompts/system/update.md"}, language, targetID)
  • return RenderSystemUpdateWithPlan(language, targetID, model.DocumentationPlan{})
    +}

+func RenderSystemUpdateWithPlan(language, targetID string, plan model.DocumentationPlan) (string, error) {

  • return RenderSystemPhaseWithPlan(model.Phase{PromptAsset: "prompts/system/update.md"}, language, targetID, plan)
    }

func RenderInstructions(profile Profile, component config.ComponentConfig, language string) (string, error) {

  • return RenderTemplateValues("templates/instructions.md", language, component.ID, map[string]string{
  • return RenderInstructionsWithPlan(profile, component, language, model.DiscoveryManifest{}, model.DocumentationPlan{})
    +}

+func RenderInstructionsWithPlan(profile Profile, component config.ComponentConfig, language string, manifest model.DiscoveryManifest, plan model.DocumentationPlan) (string, error) {

  • return RenderInstructionsWithPlanValues(profile, component, language, manifest, plan, nil)
    +}

+func RenderInstructionsWithPlanValues(profile Profile, component config.ComponentConfig, language string, manifest model.DiscoveryManifest, plan model.DocumentationPlan, overrides map[string]string) (string, error) {

  • values := map[string]string{
    "PROFILE_ID": profile.ID,
    "PROFILE_NAME": profile.DisplayName,
    "PROFILE_DESCRIPTION": profile.Description,
    @@ -104,10 +129,96 @@ func RenderInstructions(profile Profile, component config.ComponentConfig, langu
    "SCOPE": displayScope(component.Scope),
    "CANONICAL_FILES": CanonicalFilesText(profile),
    "GUIDANCE": profileGuidance(profile.ID, component.Type),
  • })
  • }
  • for key, value := range AdaptiveValues(manifest, plan) {
  •   values[key] = value
    
  • }
  • for key, value := range overrides {
  •   values[key] = value
    
  • }
  • return RenderTemplateValues("templates/instructions.md", language, component.ID, values)
    +}

+func AdaptiveValues(manifest model.DiscoveryManifest, plan model.DocumentationPlan) map[string]string {

  • const maxItems = 100
  • packs := "- None selected"
  • if len(plan.SelectedPacks) > 0 {
  •   var b strings.Builder
    
  •   for _, pack := range plan.SelectedPacks {
    
  •   	fmt.Fprintf(&b, "- `%s`\n", pack)
    
  •   }
    
  •   packs = strings.TrimRight(b.String(), "\n")
    
  • }
  • units := "- None configured or discovered"
  • if len(plan.Units) > 0 {
  •   var b strings.Builder
    
  •   for i, unit := range plan.Units {
    
  •   	if i >= maxItems {
    
  •   		fmt.Fprintf(&b, "- ... %d additional unit(s); read the plan artifact for the complete set.\n", len(plan.Units)-maxItems)
    
  •   		break
    
  •   	}
    
  •   	fmt.Fprintf(&b, "- `%s` kind=`%s` origin=`%s` roots=%s output=`%s`\n", unit.ID, unit.Kind, unit.Origin, stringList(unit.SourceRoots), unit.OutputPath)
    
  •   }
    
  •   units = strings.TrimRight(b.String(), "\n")
    
  • }
  • pages := "- No adaptive pages selected"
  • if len(plan.Pages) > 0 {
  •   var b strings.Builder
    
  •   for i, page := range plan.Pages {
    
  •   	if i >= maxItems {
    
  •   		fmt.Fprintf(&b, "- ... %d additional page(s); read the plan artifact for the complete set.\n", len(plan.Pages)-maxItems)
    
  •   		break
    
  •   	}
    
  •   	policy := ""
    
  •   	if len(page.ShardBy) > 0 {
    
  •   		policy = fmt.Sprintf(" shardBy=`%s` maximumRowsPerPage=`%d`", strings.Join(page.ShardBy, ","), page.MaximumRowsPerPage)
    
  •   	}
    
  •   	fmt.Fprintf(&b, "- `%s` view=`%s` kind=`%s`%s reason=%s\n", page.Path, page.View, page.Kind, policy, page.Reason)
    
  •   }
    
  •   pages = strings.TrimRight(b.String(), "\n")
    
  • }
  • decisions := "- No decisions"
  • if len(plan.Decisions) > 0 {
  •   var b strings.Builder
    
  •   for i, decision := range plan.Decisions {
    
  •   	if i >= maxItems {
    
  •   		fmt.Fprintf(&b, "- ... %d additional decision(s); read the plan artifact for the complete set.\n", len(plan.Decisions)-maxItems)
    
  •   		break
    
  •   	}
    
  •   	fmt.Fprintf(&b, "- `%s`: **%s** — %s\n", decision.Subject, decision.Action, decision.Reason)
    
  •   }
    
  •   decisions = strings.TrimRight(b.String(), "\n")
    
  • }
  • return map[string]string{
  •   "ADAPTIVE_PACKS":      packs,
    
  •   "DOCUMENTATION_UNITS": units,
    
  •   "ADAPTIVE_PAGES":      pages,
    
  •   "PLAN_DECISIONS":      decisions,
    
  •   "DISCOVERY_ARTIFACT":  ".wikiforge/wikiforge-discovery.json",
    
  •   "PLAN_ARTIFACT":       ".wikiforge/wikiforge-plan.json",
    
  • }
    +}

+func stringList(values []string) string {

  • if len(values) == 0 {
  •   return "[]"
    
  • }
  • return "[" + strings.Join(values, ", ") + "]"
    +}

+func RenderSystemInstructions(language, targetID string, plan model.DocumentationPlan) (string, error) {

  • values := AdaptiveValues(model.DiscoveryManifest{}, plan)
  • values["DISCOVERY_ARTIFACT"] = "sources/components/*/discovery.json"
  • values["PLAN_ARTIFACT"] = "sources/system-plan.json"
  • return RenderTemplateValues("templates/system-instructions.md", language, targetID, values)
    }

func Render(assetPath, language, targetID string, values map[string]string) (string, error) {

  • mergedValues := AdaptiveValues(model.DiscoveryManifest{}, model.DocumentationPlan{})
  • for key, value := range values {
  •   mergedValues[key] = value
    
  • }
  • values = mergedValues
    baseBytes, err := fs.ReadFile(assets.FS, "prompts/common/base.md")
    if err != nil {
    return "", err
    diff --git a/internal/prompts/supplements_test.go b/internal/prompts/supplements_test.go
    index 4fff0a501b262c2b5b0fdd4e0f20b9eefbb41b96..20d7eb405b6ddf25ca232cd1c38b2c5d7e3b69fa 100644
    --- a/internal/prompts/supplements_test.go
    +++ b/internal/prompts/supplements_test.go
    @@ -1,10 +1,12 @@
    package prompts

import (

  • "fmt"
    "strings"
    "testing"

    "github.com/example/wikiforge/internal/config"

  • "github.com/example/wikiforge/internal/model"
    )

func TestApplicationSupplementalCoverage(t *testing.T) {
@@ -169,3 +171,45 @@ func TestSplitContractUsesFreshResumePhaseIDs(t *testing.T) {
}
}
}
+
+func TestComponentPromptsAlwaysResolveAdaptivePlaceholders(t *testing.T) {

  • profile, err := GetProfile("application")
  • if err != nil {
  •   t.Fatal(err)
    
  • }
  • text, err := RenderComponentPhase(profile.Phases[0], profile, testComponent(), "English", nil)
  • if err != nil {
  •   t.Fatal(err)
    
  • }
  • for _, placeholder := range []string{"{{ADAPTIVE_PACKS}}", "{{DOCUMENTATION_UNITS}}", "{{ADAPTIVE_PAGES}}", "{{PLAN_DECISIONS}}"} {
  •   if strings.Contains(text, placeholder) {
    
  •   	t.Fatalf("unresolved %s", placeholder)
    
  •   }
    
  • }
    +}

+func TestSystemPromptsResolveAdaptivePlaceholders(t *testing.T) {

  • text, err := RenderSystemPhase(SystemPhases[0], "English", "system")
  • if err != nil {
  •   t.Fatal(err)
    
  • }
  • for _, placeholder := range []string{"{{ADAPTIVE_PACKS}}", "{{DOCUMENTATION_UNITS}}", "{{ADAPTIVE_PAGES}}", "{{PLAN_DECISIONS}}"} {
  •   if strings.Contains(text, placeholder) {
    
  •   	t.Fatalf("unresolved %s", placeholder)
    
  •   }
    
  • }
    +}

+func TestAdaptivePromptSummaryIsBoundedAndPointsToCompleteArtifact(t *testing.T) {

  • plan := model.DocumentationPlan{}
  • for i := 0; i < 150; i++ {
  •   plan.Pages = append(plan.Pages, model.PlanPage{Path: fmt.Sprintf("pages/%03d.md", i), View: "catalog", Kind: "single", Reason: "fixture"})
    
  • }
  • values := AdaptiveValues(model.DiscoveryManifest{}, plan)
  • if !strings.Contains(values["ADAPTIVE_PAGES"], "50 additional page(s)") {
  •   t.Fatalf("summary was not bounded: %s", values["ADAPTIVE_PAGES"])
    
  • }
  • if values["PLAN_ARTIFACT"] == "" {
  •   t.Fatal("missing complete plan artifact reference")
    
  • }
    +}
    diff --git a/openwiki/architecture/adaptive-planning.md b/openwiki/architecture/adaptive-planning.md
    new file mode 100644
    index 0000000000000000000000000000000000000000..bf9862e352fd791fe928a2ff675f271ad2082fda
    --- /dev/null
    +++ b/openwiki/architecture/adaptive-planning.md
    @@ -0,0 +1,94 @@
    +---
    +type: Architecture
    +title: Adaptive Planning Foundation
    +description: Configuration v3, deterministic discovery, documentation units, composable capability packs, and adaptive planning
    +resource: /internal/planner/planner.go
    +tags: [planning, discovery, configuration-v3, documentation-units]
    +---

+# Adaptive Planning Foundation
+
+WikiForge 1.3 introduces an adaptive planning layer before the existing profile renderer. The renderer remains backward-compatible in Phase 1, while the deterministic artifacts define the future hierarchical domain, component, flow, catalog, platform, engineering, operations, and system views.
+
+## Processing model
+
+```mermaid
+flowchart LR

  • C[Normalized Config v3] --> D[Deterministic Discovery]
  • D --> M[Discovery Manifest]
  • M --> P[Adaptive Planner]
  • C --> P
  • P --> A[Documentation Plan]
  • A --> R[Compatibility Renderer]
  • A --> S[System Aggregation Snapshot]
    +```

Copy link
Copy Markdown
Owner Author

WIKIFORGE_PHASE1_PATCH_PART 16/19
+The discovery implementation is in /internal/discovery/discovery.go. Planning is in /internal/planner/planner.go. Orchestration persists and injects both artifacts from /internal/orchestrator/orchestrator.go.
+
+## Documentation units
+
+A documentation unit is independent from the deployment boundary. Supported kinds are domain, subdomain, bounded-context, component, module, flow, platform, and catalog.
+
+A configured unit preserves:
+
+- component ownership;
+- relative evidence roots;
+- related units, including qualified cross-component references;
+- future output path;
+- owners and business capabilities;
+- criticality;
+- whether the unit was configured or discovered.
+
+Discovery may infer nested domain/module roots and BPMN flows. Configured source-root coverage takes precedence and suppresses duplicate inferred units.
+
+## Capability packs
+
+The planner composes capability packs from three sources:
+
+1. profile defaults;
+2. explicit components[].packs configuration;
+3. source evidence discovered inside the configured component scope.
+
+Every registered pack receives an explicit include, skip, or defer decision. A disabled required view defers the pack with a reason. Output-path collisions are also deferred explicitly rather than silently dropping a page or unit.
+
+## Evidence boundaries
+
+Discovery applies documentation.evidence.include, exclude, and maxFileSizeBytes. It skips excluded directories, symbolic links, binary files, and oversized files. The source hash is based on sorted relative paths plus eligible file contents, with no timestamps or map-order-dependent fields.
+
+## Persisted artifacts
+
+text +.wikiforge/ +├── components/<component-id>/discovery.json +├── components/<component-id>/plan.json +└── system/plan.json +
+
+Whole-system aggregation copies component documentation together with discovery.json and plan.json, and writes sources/system-plan.json. Prompts and persistent INSTRUCTIONS.md files receive bounded summaries and point to the complete artifacts.
+
+## Checkpoint invariant
+
+Source, documentation, discovery, and plan hashes are last-successful checkpoints. A failed generation records failure status and phase evidence without replacing those hashes. This prevents a later update from treating an unsuccessful generation attempt as the current documented state.
+
+## CLI
+
+text +wikiforge discover [--component ID] +wikiforge plan [--component ID] [--skip-system] [--explain] +wikiforge config migrate [--output wikiforge.v3.json] [--force] +
+
+plan --explain reports planned pages and all non-included decisions. Discovery or planning failures return a non-zero CLI exit code.
+
+## Knowledge Gaps
+
+Phase 1 does not replace the fixed profile renderer or structural validator. Hierarchical page materialization, collection sharding, semantic evidence indexing, and source-to-page change impact belong to later phases.
+
+## Source References
+
+- /internal/config/config.go
+- /internal/discovery/discovery.go
+- /internal/planner/planner.go
+- /internal/orchestrator/orchestrator.go
+- /internal/prompts/prompts.go
+- /schema/wikiforge-config.schema.json
diff --git a/openwiki/architecture/config-model.md b/openwiki/architecture/config-model.md
index 6fe500c987d928312e4c3bebbc242e6c4469e123..7627bf417afa2086e8df513744f704be5ab07990 100644
--- a/openwiki/architecture/config-model.md
+++ b/openwiki/architecture/config-model.md
@@ -1,171 +1,232 @@

type: Reference
title: WikiForge Configuration Model
-description: YAML/JSON configuration schema, component types, profile selection, path normalization, and validation rules
-tags: [configuration, yaml, components, profiles]
+description: Configuration v3, compatibility migration, components, documentation units, views, capability packs, evidence boundaries, and validation
+tags: [configuration, yaml, components, documentation-units, adaptive-planning]
resource: /internal/config/config.go

Configuration Model

-WikiForge uses a YAML or JSON configuration file (default wikiforge.yaml) with a version 2 schema and backward-compatible v1 migration.
+WikiForge loads YAML or JSON from wikiforge.yaml by default. The normalized in-memory model is version 3. Version 1 services and version 2 components remain accepted and are converted before validation.

Config structure

-The top-level Config struct has these sections:

-version: 2                # 1 or 2 (v1 services are migrated to components)
-workspace: .              # working directory root
-openwiki: { ... }         # OpenWiki command, args, timeout, environment
-execution: { ... }        # parallelism, retries, repair rounds, failure policy
-documentation: { ... }    # quality thresholds, validation toggles
-mermaid: { ... }          # Mermaid CLI config (render/basic/off mode)
-components: [ ... ]       # component definitions
-system: { ... }           # whole-system aggregation config
+version: 3
+workspace: .
+openwiki: { ... }
+execution: { ... }
+documentation:
+  language: English
+  views: [system, domain, component, flow, catalog, platform, engineering, operations]
+  catalogs:
+    shardBy: [domain, owner]
+    maximumRowsPerPage: 150
+  evidence:
+    include: ["**"]
+    exclude: [.git/**, .wikiforge/**, openwiki/**, vendor/**, node_modules/**, generated/**]
+    maxFileSizeBytes: 2097152
+mermaid: { ... }
+components: [ ... ]
+documentationUnits: [ ... ]
+system: { ... }

-### OpenWiki config
+## OpenWiki config

Field Default Description
command npx OpenWiki executable
- args ["--yes", "openwiki@0.2.0", "code"]
- modelId ""
- timeoutMinutes 60
- environment {OPENWIKI_TELEMETRY_DISABLED: "1", OPENWIKI_PROVIDER_RETRY_ATTEMPTS: "3"}
+ args --yes openwiki@0.2.0 code
+ modelId empty
+ timeoutMinutes 60
+ environment telemetry disabled, provider retries 3

-### Execution config
+## Execution config

| Field | Default | Description |
-|---|---|---|
-| parallelComponents | 2 | Max concurrent component groups |
-| maxProcessRetries | 2 | Max retries per phase process |
-| maxRepairRounds | 2 | Max validation/repair cycles |
-| continueOnComponentFailure | true | Keep going after component failure |
+|---|---:|---|
+| parallelComponents | 2 | Maximum repository groups processed concurrently |
+| maxProcessRetries | 2 | Additional retry attempts for retryable child-process failures |
+| maxRepairRounds | 2 | Maximum targeted validation-repair rounds |
+| continueOnComponentFailure | true | Continue processing independent components after failure |
+
+Legacy parallelServices and continueOnServiceFailure are read and normalized.
+
+## Documentation config
+
+The existing quality and Mermaid-related fields remain. Version 3 adds the planning fields below.
+
+### Views
+
+Supported values:
+
+- system;
+- domain;
+- component;
+- flow;
+- catalog;
+- platform;
+- engineering;
+- operations.

-### Documentation config
+Disabling a view does not silently discard a selected capability. The planner records a defer decision for pages or units that require that view. quickstart.md remains planned as the bounded component entry point even when the detailed component view is disabled.
+
+### Catalog policy

Field Default Description
- language English
- minimumQualityScore 85
- requireFrontMatter true
- requireSourceReferences true
- validateSourcePaths true
- allowedDiagramTypes 9 types
+ shardBy domain, owner
+ maximumRowsPerPage 150

+Supported shard dimensions are domain, subdomain, bounded-context, component, owner, repository, runtime, transport, data-store, and criticality.

-### Mermaid config
+### Evidence policy

Field Default Description
- mode render
- command npx
- args ["--yes", "@mermaid-js/mermaid-cli@11.12.0", "-i", "{input}", "-o", "{output}", "--quiet"]
- timeoutSeconds 90
+ include **
+ exclude Git, WikiForge, generated, dependency, and build directories
+ maxFileSizeBytes 2 MiB

+Discovery does not follow symbolic links and skips binary or oversized files. A missing component root is an error; an unreadable child path is recorded as an unknown evidence gap.

Component configuration

components:
-  - id: order-service
-    type: microservice
-    repository: ./repositories/order-service
+  - id: commerce-core
+    type: modular-monolith
+    repository: ./repositories/commerce-core
+    scope: .
	    enabled: true
+	    includeInSystem: true
	    group: commerce
-	    tags: [order, deployable]
+	    tags: [core]
	    dependsOn: [shared-contracts]
-	    scope: ""                       # empty = repository root
-	    includeInSystem: true           # default: true
+	    owners: [commerce-team]
+	    capabilities: [order-management, pricing]
+	    packs: [workflow, messaging, database]

-### Component types and profile mapping
+repository and scope define the runtime source boundary. They do not define the documentation decomposition.

- Type Profile Description
- monolith, microservice, worker, gateway, frontend, cli application Deployable application
- modular-monolith modular-application Modular monolith with module docs
- library, shared-library, internal-library, framework, sdk reusable Library, SDK, framework
- iac, infrastructure, gitops, deployment, platform infrastructure IaC, GitOps, platform
- configuration, shared-config, config configuration Shared config/policy
- contract, contracts, schema, schemas contracts API/event/data schemas
- generic, repository generic Unclassified fallback

-## Profiles

-Seven documentation profiles each define a set of phases with required output files, sections, and diagram types. Profile definitions are in internal/prompts/profiles.go.

-### Phase structure

-Each phase has:
-- ID (e.g., A10, M25, I70)
-- Name (e.g., "Architecture", "Domain behaviour")
-- Output file (e.g., architecture/overview.md)
-- Objective — Describes the phase purpose
-- Required headings — Exact section headings to include
-- Required diagram type — Mermaid diagram contract (flowchart, sequenceDiagram, erDiagram, classDiagram, or any)
-- Page contracts — For specialized catalog phases

-### Phase IDs by profile

-Every profile follows the pattern:
-- {PREFIX}00 — Bootstrap and quickstart
-- {PREFIX}10–{PREFIX}70 — Core phases
-- {PREFIX}S01–{PREFIX}SNN — Specialized catalog batches (inserted before consolidate)
-- {PREFIX}C90 — Consolidate/relationship audit

Copy link
Copy Markdown
Owner Author

WIKIFORGE_PHASE1_PATCH_PART 17/19
-Prefixes: A (application), M (modular-application), R (reusable), I (infrastructure), C (configuration), K (contracts), G (generic).

-### Profile contracts

- Profile Core pages Specialized pages Total canonical pages Minimum Mermaid blocks
- application 8 22 30 5
- modular-application 9 22 31 6
- reusable 8 19 27 5
- infrastructure 8 15 23 5
- configuration 8 11 19 5
- contracts 8 10 18 5
- generic 8 22 30 5
+### Type-to-profile mapping

+| Types | Profile |
+|---|---|
+| monolith, microservice, worker, gateway, frontend, cli | application |
+| modular-monolith | modular-application |
+| library, shared-library, internal-library, framework, sdk | reusable |
+| iac, infrastructure, gitops, deployment, platform | infrastructure |
+| configuration, shared-config, config | configuration |
+| contract, contracts, schema, schemas | contracts |
+| generic, repository | generic |
+
+An explicit profile may override the type mapping when it names a supported profile.
+
+## Documentation units
+
+```yaml
+documentationUnits:

    • id: order-management
  • component: commerce-core
  • kind: domain
  • sourceRoots: [modules/order, workflows/order]
  • relatedUnits: [pricing, fulfilment/dispatch]
  • output: domains/order-management
  • owners: [commerce-team]
  • capabilities: [order-management]
  • criticality: high
    +```

+Supported kinds are domain, subdomain, bounded-context, component, module, flow, platform, and catalog.
+
+Invariants:
+
+- IDs are unique within a component, not globally;
+- cross-component relations use component/unit when necessary;
+- a unit must reference an enabled component;
+- relative source roots and output paths cannot escape through .. or absolute paths;
+- two units in one component cannot claim the same configured output;
+- criticality is empty, low, medium, high, or critical;
+- explicit units take precedence over inferred units covered by the same source root.
+
+## Capability packs
+
+Supported packs:
+
+text +api, cache, concurrency, configuration, container-runtime, cryptography, +data-access, database, distributed-coordination, domain, engineering, +files, jobs, messaging, migrations, rate-limit, runtime, security, +telemetry, workflow +
+
+The adaptive planner unions profile defaults, explicit components[].packs, and discovered packs. Every registered pack has a canonical planning mapping and an include, skip, or defer decision.

Path normalization

-When loading a configuration, WikiForge normalizes all paths:
+- workspace, component repositories, system output, and facts paths become absolute paths relative to the config file.
+- scope, unit source roots, and unit outputs remain normalized relative paths.
+- portable component and unit IDs are enforced across Windows, Linux, and macOS.
+- config migrate emits paths relative to the output config location when possible, avoiding machine-specific absolute paths.

-1. Workspace — Resolved to absolute path relative to config file directory.
-2. Repository — Same resolution, supports repo-relative and absolute paths.
-3. Scope — Normalized with pathutil.NormalizeRelative: accepts / or \, rejects absolute paths and parent escapes.
-4. System output — Resolved to absolute path.
-5. Facts path — Resolved if non-empty.
+## Compatibility migration

-The ComponentConfig.WorkDir() method returns {repository}/{scope} (or just {repository} if scope is empty). The DocumentationRoot() method returns {workdir}/openwiki.
+text +version 1 services -> microservice components -> normalized version 3 +version 2 components -> normalized version 3 defaults +version 3 -> normalized and validated version 3 +
+
+Use:
+
+bash +wikiforge config migrate --config wikiforge.yaml --output wikiforge.v3.json +

-## Configuration validation
+The command refuses to replace an existing output unless --force is provided. The generated JSON is reloadable by WikiForge.

-config.Validate() checks:
+## Validation

-- Version is 1 or 2
-- All components have non-empty IDs
-- OpenWiki command is non-empty
-- Documentation minimum quality score is 0–100
-- Mermaid timeout is positive
-- Component IDs are portable path segments (no spaces, special chars, etc.)
+config.Validate enforces:

-## V1 backward compatibility
+- current normalized version;
+- at least one enabled component;
+- portable and unique component IDs;
+- valid profiles, packs, views, unit kinds, criticalities, and shard dimensions;
+- unique component work directories;
+- valid dependency and related-unit references;
+- safe relative scopes, source roots, and output paths;
+- positive catalog and evidence limits;
+- required system output when system aggregation is enabled;
+- valid Mermaid mode.

-Configs with version: 1 or legacy services array are migrated:
-- version: 0 (unset) → treated as v1
-- Each services[].{id, path, enabled} is converted to components[] with type: microservice
+The published Draft 2020-12 schema is /schema/wikiforge-config.schema.json. Tests prevent the schema pack/view enums from drifting from the Go registry and load all published version 3 examples through the production parser.

Source map

File Role
- /internal/config/config.go
- /internal/config/yaml.go
- /internal/config/config_test.go
- /schema/wikiforge-config.schema.json
- /wikiforge.example.yaml
- /internal/prompts/profiles.go
- /internal/prompts/supplements.go
- /internal/pathutil/pathutil.go
+ /internal/config/config.go
+ /internal/config/yaml.go
+ /internal/config/config_test.go
+ /internal/config/schema_test.go
+ /schema/wikiforge-config.schema.json
+ /wikiforge.example.yaml
+ /internal/discovery/discovery.go
+ /internal/planner/planner.go

+## Knowledge Gaps
+
+The schema validates shape and enumerated values but cannot validate filesystem existence or Git work-tree status. wikiforge doctor performs those environmental checks, including configured documentation-unit source roots.
+
+## Source References
+
+- /internal/config/config.go
+- /internal/config/config_test.go
+- /internal/config/schema_test.go
+- /schema/wikiforge-config.schema.json
+- /internal/cli/cli.go
diff --git a/openwiki/architecture/index.md b/openwiki/architecture/index.md
index 09b4d91fceed437445fe0558e02c12e36c731d24..e75b0525c16565714559889292b1b442111bc62c 100644
--- a/openwiki/architecture/index.md
+++ b/openwiki/architecture/index.md
@@ -8,3 +8,4 @@ description: "Files and subdirectories in Architecture."

  • WikiForge Configuration Model - YAML/JSON configuration schema, component types, profile selection, path normalization, and validation rules
  • WikiForge Architecture Overview - High-level architecture, component model, phase lifecycle, state management, and cross-platform path handling in WikiForge
    +- Adaptive Planning Foundation - Configuration v3, deterministic discovery, documentation units, composable packs, and adaptive planning
    diff --git a/openwiki/architecture/overview.md b/openwiki/architecture/overview.md
    index 7c5bfa5765969fa2ae630537d084fe6cd7a2fa9e..91e6629d305e3fc7e9286e01573815070d7bd519 100644
    --- a/openwiki/architecture/overview.md
    +++ b/openwiki/architecture/overview.md
    @@ -22,17 +22,17 @@ cmd/wikiforge/main.go

CLI routing

-internal/cli/cli.go routes all commands from the CLI.Run method. Each command (init, doctor, profiles, plan, generate, update, resume, validate, graph) has its own handler. The --component flag (backward-compatible --service alias) selects individual components.
+internal/cli/cli.go routes all commands from the CLI.Run method. Each command (init, doctor, profiles, config migrate, discover, plan, generate, update, resume, validate, graph) has its own handler. The --component flag (backward-compatible --service alias) selects individual components.

Key default values are defined here:
-- Version: 1.2.3 (constant)
+- Version: 1.3.0 (constant)

  • Config file: wikiforge.yaml (overridable with --config)

Configuration subsystem

internal/config/config.go provides the full configuration model:

-- Version 2 config with backward-compatible v1 servicescomponents migration.
+- Version 3 config with backward-compatible v1 services and v2 component normalization, explicit documentation units, composable capability packs, views, evidence boundaries, and shard policy.

  • Custom YAML subset parser at internal/config/yaml.go — minimal indentation-based parser that avoids full YAML library dependencies for generated configs.
  • JSON Schema at /schema/wikiforge-config.schema.json for editor validation.
  • Defaults applied automatically (parallelism, timeouts, Mermaid mode, etc.).
    @@ -53,10 +53,17 @@ Every enabled ComponentConfig has:
    | group | Optional logical grouping |
    | tags | Classification tags |
    | dependsOn | Declared component dependencies |
    +| owners | Ownership hints |
    +| capabilities | Business capabilities converted to configured domain units |
    +| packs | Explicit capability packs composed with profile defaults and discovery |
    | includeInSystem | Whether to include in whole-system aggregation |

Multiple components may share one repository with different scope values. They are automatically serialized during generation to avoid competing OpenWiki writes.

+## Discovery and adaptive planning
+
+Before generation, WikiForge scans the normalized component scope using configured evidence include/exclude rules. It writes deterministic discovery.json and plan.json artifacts under .wikiforge/components/<id>/. The planner separates component boundaries from documentation units, combines profile, explicit, and discovered capability packs, and records include/skip/defer decisions. See Adaptive planning.
+

Profile and phase system

Seven documentation profiles define phase contracts:
@@ -95,7 +102,7 @@ Components sharing a Git repository are deliberately placed in the same group an
internal/state/store.go persists run state to .wikiforge/state.json in JSON format with atomic file writes (write-to-tmp, rename). The state includes:

  • Run ID, mode (generate/update), start time
    -- Per-component state — Git HEAD, documentation hash, source hash, phase statuses
    +- Per-component state — Git HEAD, last-successful documentation/source/discovery/plan hashes, status, and phase statuses
  • System state — Phase statuses for whole-system phases

The state enables:
diff --git a/openwiki/integrations/ci-cd.md b/openwiki/integrations/ci-cd.md

Copy link
Copy Markdown
Owner Author

WIKIFORGE_PHASE1_PATCH_PART 18/19
index 734e9aee7ff170e320d5bf78a98a913a7674de19..6022769055060589cecd4f074c7f6c4f6eeb5374 100644
--- a/openwiki/integrations/ci-cd.md
+++ b/openwiki/integrations/ci-cd.md
@@ -1,14 +1,14 @@

type: Playbook
title: CI/CD and Release Operations
-description: GitHub Actions workflows for CI testing, OpenWiki scheduled updates, and GoReleaser-powered binary releases
+description: GitHub Actions workflows for multi-platform CI testing and GoReleaser-powered binary releases
tags: [ci, cd, github-actions, goreleaser, release]
resource: /.github/workflows

CI/CD and Release Operations

-WikiForge has three GitHub Actions workflows, a GoReleaser configuration, a Dockerfile, and documented release procedures.
+WikiForge has two GitHub Actions workflows, a GoReleaser configuration, a Dockerfile, and documented release procedures.

CI workflow

@@ -18,9 +18,10 @@ WikiForge has three GitHub Actions workflows, a GoReleaser configuration, a Dock

  • Steps:
    1. actions/checkout@v4
    2. actions/setup-go@v5 with Go 1.23.x
    1. go test ./... (all OS)
    1. go test -race ./... (Linux only, for race detection)
    1. go build ./cmd/wikiforge
    1. go test -count=1 ./... (all OS)
    1. go vet ./... (all OS)
    1. go test -race -count=1 ./... (Linux only, for race detection)
    1. go build ./cmd/wikiforge

This is a verification workflow only. It does not publish binaries.

@@ -39,8 +40,8 @@ This is a verification workflow only. It does not publish binaries.

git pull --ff-only
-git tag v1.2.4
-git push origin v1.2.4
+git tag v<semver>
+git push origin v<semver>

Manual re-release

@@ -71,23 +72,11 @@ SHA-256 checksums are uploaded as checksums.txt.

Environment: PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium, OPENWIKI_TELEMETRY_DISABLED=1.

-## OpenWiki scheduled update

-/.github/workflows/openwiki-update.yml runs daily at 08:00 UTC:

-1. Check out repository
-2. Install Node.js 22
-3. Install OpenWiki globally
-4. Run openwiki code --update --print (with OpenRouter provider and GLM-5.2 model)
-5. Create a pull request with changes under openwiki/, AGENTS.md, CLAUDE.md, and the workflow file itself

-This workflow is the mechanism for keeping the repository's own documentation fresh.

Build verification

-/BUILD-VERIFICATION.md documents the verified build process for release 1.2.3:
+/BUILD-VERIFICATION.md documents the verified build process for the current source revision:

-- go test ./... and go test -race ./... passed
+- go test -count=1 ./... and go test -race -count=1 ./... passed

  • go vet ./... passed
  • OpenWiki executable runner contract tests passed
  • Large-prompt transport regression tested (160 KB prompts, <4 KiB arguments)
    @@ -109,8 +98,8 @@ The /RELEASING.md document records the full release procedure

Pre-tag verification

-go test ./...
-go test -race ./...
+go test -count=1 ./...
+go test -race -count=1 ./...
go vet ./...
goreleaser release --snapshot --clean

@@ -121,7 +110,6 @@ goreleaser release --snapshot --clean
|---|---|
| /.github/workflows/ci.yml | Multi-OS CI verification |
| /.github/workflows/release.yml | Tag-triggered GoReleaser release |
-| /.github/workflows/openwiki-update.yml | Scheduled OpenWiki doc update |
| /.goreleaser.yaml | GoReleaser cross-compilation config |
| /Dockerfile | Multi-stage Docker build |
| /RELEASING.md | Release procedure documentation |
diff --git a/openwiki/quickstart.md b/openwiki/quickstart.md
index 98b119da34f2fca1e4e2d95a4a2d864689c497ec..66bc2c0a92b74512d69261bd02c5d65eb6158e75 100644
--- a/openwiki/quickstart.md
+++ b/openwiki/quickstart.md
@@ -8,11 +8,12 @@ resource: /cmd/wikiforge/main.go

WikiForge Documentation

-WikiForge is a cross-platform Go CLI (v1.2.3) that orchestrates OpenWiki in controlled phases to generate validated documentation for software repositories. It supports multiple documentation profiles, monorepo scoping, cross-repository parallelism, validation and repair, whole-system aggregation, and knowledge-graph export.
+WikiForge is a cross-platform Go CLI (v1.3.0) that orchestrates OpenWiki in controlled phases to generate validated documentation for software repositories. It supports multiple documentation profiles, monorepo scoping, cross-repository parallelism, validation and repair, whole-system aggregation, and knowledge-graph export.

Key capabilities:

-- Profile-driven generation — 7 documentation profiles (application, modular-application, reusable, infrastructure, configuration, contracts, generic) each with phase-specific contracts, required sections, and diagram types.
+- Adaptive planning — Deterministic discovery, documentation units, composable capability packs, explicit include/skip/defer decisions, and persisted plans.
+- Profile-driven compatibility generation — 7 documentation profiles (application, modular-application, reusable, infrastructure, configuration, contracts, generic) each with phase-specific contracts, required sections, and diagram types.

  • Phased orchestration — Each component progresses through up to 10+ phases (init → overview → architecture → domain → interfaces → data → security → development → specialized catalogs → consolidate), with each phase owning one canonical page.
  • Monorepo support — Multiple scoped components in one repository with automatic serialization to avoid competing OpenWiki writes.
  • Validation and repair — Validates front matter, required sections, Mermaid diagrams (including rendering), source references, and relative links. Runs targeted repair rounds using validator findings.
    @@ -29,13 +30,13 @@ Download from [GitHub Releases](https://github.com/fajarnugraha37/wikiforge/rele

Windows:

-Expand-Archive .\wikiforge-1.2.3-windows-amd64.zip
-cd .\wikiforge-1.2.3-windows-amd64
+Expand-Archive .\wikiforge-<version>-windows-amd64.zip
+cd .\wikiforge-<version>-windows-amd64

Linux/macOS:

-unzip wikiforge-1.2.3-linux-amd64.zip
+unzip wikiforge-<version>-linux-amd64.zip
cd linux-amd64

@@ -62,10 +63,11 @@ export OPENWIKI_MODEL_ID=cheap-code-model
./wikiforge doctor


-### 5. Preview the phase plan
+### 5. Discover and explain the adaptive plan

```bash
-./wikiforge plan
+./wikiforge discover
+./wikiforge plan --explain

6. Generate all wikis

@@ -104,7 +106,8 @@ Generate a single component:

Section Description
Architecture overview High-level architecture, component model, profiles, state store, paths
- Configuration model
+ Configuration model
+ Adaptive planning
Generation pipeline End-to-end generate/update/resume workflow, validation, repair, reports, graph
Prompt system Prompt assets, phase contracts, specialized catalogs, system phases
OpenWiki bridge Child-process execution, prompt bridge protocol, cross-platform path safety
@@ -115,7 +118,11 @@ Generate a single component:

Concepts at a glance

  • Component — An independently documented repository scope (may be a whole repo or a monorepo subdirectory).
    -- Profile — A documentation contract with required phases, pages, sections, and diagram types. Seven profiles exist: application, modular-application, reusable, infrastructure, configuration, contracts, generic.
    +- Documentation unit — A domain, bounded context, module, flow, platform area, or catalog documented independently from deployment boundaries.
    +- Capability pack — A composable concern selected by profile, explicit configuration, or source evidence.
    +- Discovery manifest — Deterministic component-scope evidence and inferred-unit artifact.
    +- Documentation plan — Explicit future page paths and include/skip/defer decisions.
    +- Profile — A backward-compatible documentation contract with required phases, pages, sections, and diagram types. Seven profiles exist: application, modular-application, reusable, infrastructure, configuration, contracts, generic.
  • Phase — A single OpenWiki invocation that owns one canonical Markdown page with required sections and diagram.
  • Canonical pages — Profile-specific Markdown files with enforced sections, front matter, and diagram contracts.
  • Specialized catalogs — Additional optional pages with exact table-header contracts (e.g., endpoint catalogs, job catalogs, dependency matrices).
    @@ -139,8 +146,10 @@ Generate a single component:
    |---|---|
    | init | Generate a default wikiforge.yaml configuration |
    | doctor | Validate prerequisites and component scopes |
    -| profiles | List supported types and documentation profiles |
    -| plan | Preview the phase plan without executing |
    +| profiles | List supported types, profiles, capability packs, and views |
    +| config migrate | Normalize a v1/v2/v3 config into portable v3 JSON |
    +| discover | Produce deterministic discovery manifests and component plans |
    +| plan | Preview adaptive pages and, with --explain, all planning decisions |
    | generate | Generate all component wikis and the whole-system wiki |
    | update | Incremental update of existing documentation |
    | resume | Resume a cancelled or failed generation |
    @@ -150,4 +159,4 @@ Generate a single component:

Backlog

-No areas deferred. The initial documentation set covers all major domains of WikiForge 1.2.3.
+Phase 1 intentionally retains the fixed profile renderer. Hierarchical materialization, catalog sharding, semantic evidence indexing, and impact-based updates are deferred to the subsequent implementation phases and are not represented as completed capabilities here.
diff --git a/schema/wikiforge-config.schema.json b/schema/wikiforge-config.schema.json
index 99d8f43bd60f341cc750c3907c4d42f4083aa174..7f7b9385646ee6d3499ae33d52a0ea95cc91cbd8 100644
--- a/schema/wikiforge-config.schema.json
+++ b/schema/wikiforge-config.schema.json
@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",

  •    2
    
  •    2,
    
  •    3
     ]
    
    },
    "workspace": {
    @@ -32,7 +33,8 @@
    "type": "array",
    "items": {
    "type": "string"
  •      }
    
  •      },
    
  •      "uniqueItems": true
       },
       "modelId": {
         "type": "string"
    

@@ -117,7 +119,77 @@
"items": {
"type": "string"
},

  •      "minItems": 1
    
  •      "uniqueItems": true
    
  •    },
    
  •    "views": {
    
  •      "type": "array",
    
  •      "items": {
    
  •        "type": "string",
    
  •        "enum": [
    
  •          "system",
    
  •          "domain",
    
  •          "component",
    
  •          "flow",
    
  •          "catalog",
    
  •          "platform",
    
  •          "engineering",
    
  •          "operations"
    
  •        ]
    
  •      },
    
  •      "uniqueItems": true
    
  •    },
    

Copy link
Copy Markdown
Owner Author

WIKIFORGE_PHASE1_PATCH_PART 19/19

  •   "catalogs": {
    
  •     "type": "object",
    
  •     "properties": {
    
  •   	"shardBy": {
    
  •   	  "type": "array",
    
  •   	  "items": {
    
  •   		"type": "string",
    
  •   		"enum": [
    
  •   		  "domain",
    
  •   		  "subdomain",
    
  •   		  "bounded-context",
    
  •   		  "component",
    
  •   		  "owner",
    
  •   		  "repository",
    
  •   		  "runtime",
    
  •   		  "transport",
    
  •   		  "data-store",
    
  •   		  "criticality"
    
  •   		]
    
  •   	  },
    
  •   	  "uniqueItems": true
    
  •   	},
    
  •   	"maximumRowsPerPage": {
    
  •   	  "type": "integer",
    
  •   	  "minimum": 1
    
  •   	}
    
  •     },
    
  •     "additionalProperties": false
    
  •   },
    
  •   "evidence": {
    
  •     "type": "object",
    
  •     "properties": {
    
  •   	"include": {
    
  •   	  "type": "array",
    
  •   	  "items": {
    
  •   		"type": "string"
    
  •   	  },
    
  •   	  "uniqueItems": true
    
  •   	},
    
  •   	"exclude": {
    
  •   	  "type": "array",
    
  •   	  "items": {
    
  •   		"type": "string"
    
  •   	  },
    
  •   	  "uniqueItems": true
    
  •   	},
    
  •   	"maxFileSizeBytes": {
    
  •   	  "type": "integer",
    
  •   	  "minimum": 1
    
  •   	}
    
  •     },
    
  •     "additionalProperties": false
      }
    },
    "additionalProperties": false
    

@@ -139,7 +211,8 @@
"type": "array",
"items": {
"type": "string"

  •     }
    
  •     },
    
  •     "uniqueItems": true
      },
      "timeoutSeconds": {
        "type": "integer",
    

@@ -155,6 +228,12 @@
"$ref": "#/$defs/component"
}
},

  • "documentationUnits": {
  • "type": "array",
    
  • "items": {
    
  •   "$ref": "#/$defs/documentationUnit"
    
  • }
    
  • },
    "services": {
    "type": "array",
    "deprecated": true,
    @@ -184,7 +263,8 @@
    "type": "array",
    "items": {
    "type": "string"
  •     }
    
  •     },
    
  •     "uniqueItems": true
      }
    },
    "additionalProperties": false
    

@@ -202,6 +282,7 @@
]
}
],

  • "additionalProperties": false,
    "$defs": {
    "component": {
    "type": "object",
    @@ -216,8 +297,7 @@
    },
    "type": {
    "type": "string",
  •     "minLength": 1,
    
  •     "description": "Built-in types auto-select a profile; custom types may use an explicit profile and otherwise fall back to generic."
    
  •     "minLength": 1
      },
      "profile": {
        "enum": [
    

@@ -254,13 +334,58 @@
"type": "array",
"items": {
"type": "string"

  •     }
    
  •     },
    
  •     "uniqueItems": true
      },
      "dependsOn": {
        "type": "array",
        "items": {
      	"type": "string"
    
  •     }
    
  •     },
    
  •     "uniqueItems": true
    
  •   },
    
  •   "owners": {
    
  •     "type": "array",
    
  •     "items": {
    
  •   	"type": "string"
    
  •     },
    
  •     "uniqueItems": true
    
  •   },
    
  •   "capabilities": {
    
  •     "type": "array",
    
  •     "items": {
    
  •   	"type": "string"
    
  •     },
    
  •     "uniqueItems": true
    
  •   },
    
  •   "packs": {
    
  •     "type": "array",
    
  •     "items": {
    
  •   	"type": "string",
    
  •   	"enum": [
    
  •   	  "api",
    
  •   	  "cache",
    
  •   	  "concurrency",
    
  •   	  "configuration",
    
  •   	  "container-runtime",
    
  •   	  "cryptography",
    
  •   	  "data-access",
    
  •   	  "database",
    
  •   	  "distributed-coordination",
    
  •   	  "domain",
    
  •   	  "engineering",
    
  •   	  "files",
    
  •   	  "jobs",
    
  •   	  "messaging",
    
  •   	  "migrations",
    
  •   	  "rate-limit",
    
  •   	  "runtime",
    
  •   	  "security",
    
  •   	  "telemetry",
    
  •   	  "workflow"
    
  •   	]
    
  •     },
    
  •     "uniqueItems": true
      }
    },
    "additionalProperties": false,
    

@@ -281,6 +406,77 @@
}
]
},

  • "documentationUnit": {
  • "type": "object",
    
  • "required": [
    
  •   "id",
    
  •   "component",
    
  •   "kind"
    
  • ],
    
  • "properties": {
    
  •   "id": {
    
  •     "type": "string",
    
  •     "minLength": 1
    
  •   },
    
  •   "component": {
    
  •     "type": "string",
    
  •     "minLength": 1
    
  •   },
    
  •   "kind": {
    
  •     "enum": [
    
  •   	"domain",
    
  •   	"subdomain",
    
  •   	"bounded-context",
    
  •   	"component",
    
  •   	"module",
    
  •   	"flow",
    
  •   	"platform",
    
  •   	"catalog"
    
  •     ]
    
  •   },
    
  •   "sourceRoots": {
    
  •     "type": "array",
    
  •     "items": {
    
  •   	"type": "string"
    
  •     },
    
  •     "uniqueItems": true
    
  •   },
    
  •   "relatedUnits": {
    
  •     "type": "array",
    
  •     "items": {
    
  •   	"type": "string"
    
  •     },
    
  •     "uniqueItems": true
    
  •   },
    
  •   "output": {
    
  •     "type": "string"
    
  •   },
    
  •   "owners": {
    
  •     "type": "array",
    
  •     "items": {
    
  •   	"type": "string"
    
  •     },
    
  •     "uniqueItems": true
    
  •   },
    
  •   "capabilities": {
    
  •     "type": "array",
    
  •     "items": {
    
  •   	"type": "string"
    
  •     },
    
  •     "uniqueItems": true
    
  •   },
    
  •   "criticality": {
    
  •     "type": "string",
    
  •     "enum": [
    
  •   	"low",
    
  •   	"medium",
    
  •   	"high",
    
  •   	"critical"
    
  •     ]
    
  •   }
    
  • },
    
  • "additionalProperties": false
    
  • },
    "legacyService": {
    "type": "object",
    "required": [
    diff --git a/wikiforge.example.yaml b/wikiforge.example.yaml
    index df2d27f19937fb229771c3e5db0e055ace99ad09..e2e80944b09f36ca5f7da794177638de64f1259b 100644
    --- a/wikiforge.example.yaml
    +++ b/wikiforge.example.yaml
    @@ -1,13 +1,9 @@
    -version: 2
    +version: 3
    workspace: .

openwiki:

  • Pinned for reproducible runs. Replace with your approved command/model.

    command: npx
  • args:
    • --yes
    • openwiki@0.2.0
    • code
  • args: [--yes, openwiki@0.2.0, code]
    modelId: ""
    timeoutMinutes: 60
    environment:
    @@ -15,8 +11,6 @@ openwiki:
    OPENWIKI_PROVIDER_RETRY_ATTEMPTS: "3"

execution:

  • Different repositories may run concurrently. Components sharing one

  • repository are automatically serialized, which is safe for monorepos.

    parallelComponents: 2
    maxProcessRetries: 2
    maxRepairRounds: 2
    @@ -25,99 +19,67 @@ execution:
    documentation:
    language: English
    minimumQualityScore: 85
  • 0 = use the selected component profile's built-in contract.

    minimumPages: 0
    requireFrontMatter: true
    requireSourceReferences: true
    validateSourcePaths: true
    requireMermaid: true
  • 0 = use the selected component profile's built-in minimum.

    minimumMermaidBlocks: 0
  • allowedDiagramTypes:
    • flowchart
    • sequenceDiagram
    • stateDiagram-v2
    • erDiagram
    • classDiagram
    • architecture-beta
    • gitGraph
    • mindmap
  • allowedDiagramTypes: [flowchart, sequenceDiagram, stateDiagram-v2, erDiagram, classDiagram, architecture-beta, gitGraph, mindmap]
  • Phase 1 planning views. Phase 2 will render this hierarchy directly.

  • views: [system, domain, component, flow, catalog, platform, engineering, operations]
  • catalogs:
  • shardBy: [domain, owner]
  • maximumRowsPerPage: 150
  • evidence:
  • include: ["**"]
  • exclude: [.git/, .wikiforge/, openwiki/, vendor/, node_modules/, dist/, build/, target/, generated/, .wikiforge-prompt-*.md, "/*.bin"]
  • maxFileSizeBytes: 2097152

mermaid:

  • render = parse every Mermaid block with the pinned official CLI.

  • basic = offline structural checks only.

  • off = do not validate Mermaid.

    mode: render
    command: npx
  • args:
    • --yes
    • "@mermaid-js/mermaid-cli@11.12.0"
    • -i
    • "{input}"
    • -o
    • "{output}"
    • --quiet
  • args: [--yes, "@mermaid-js/mermaid-cli@11.12.0", -i, "{input}", -o, "{output}", --quiet]
    timeoutSeconds: 90

-# A component is any independently documented repository scope. Multiple
-# components may share one repository by using different relative scopes.
components:

  • Conventional microservice repository.

    • id: order-service
  • type: microservice
  • repository: ./repositories/order-service
  • enabled: false
  • group: commerce
  • tags: [order, deployable]
  • dependsOn: [shared-contracts]
  • Entire monolith repository.

    • id: legacy-commerce
  • type: monolith
  • repository: ./repositories/legacy-commerce
  • enabled: false
  • Modular monolith with module-aware documentation.

    • id: commerce-core
      type: modular-monolith
      repository: ./repositories/commerce-core
  • enabled: false
  • Multiple scoped components inside one monorepo.

    • id: catalog-app
  • type: microservice
  • repository: ./repositories/platform-monorepo
  • scope: apps/catalog
  • enabled: false
  • enabled: true
  • owners: [commerce-team]
  • capabilities: [order-management, pricing, fulfilment]
  • Explicit packs are composed with profile defaults and discovered packs.

  • packs: [workflow, messaging, database, migrations]
  • id: shared-runtime
    type: framework
    repository: ./repositories/platform-monorepo
    scope: packages/runtime
    enabled: false
  • owners: [platform-team]
  • packs: [concurrency, telemetry]
    • id: shared-contracts
  • type: contracts
  • repository: ./repositories/platform-monorepo
  • scope: contracts
  • enabled: false
  • Infrastructure/IaC/GitOps repository.

    • id: production-infrastructure
  • type: iac
  • repository: ./repositories/production-infrastructure
  • enabled: false
    +documentationUnits:
    • id: order-management
  • component: commerce-core
  • kind: domain
  • sourceRoots: [modules/order, workflows/order]
  • output: domains/order-management
  • owners: [commerce-team]
  • capabilities: [order-management]
  • criticality: high
  • Shared configuration repository.

    • id: platform-configuration
  • type: configuration
  • repository: ./repositories/platform-configuration
  • enabled: false
    • id: submit-order
  • component: commerce-core
  • kind: flow
  • sourceRoots: [workflows/order/submit-order.bpmn]
  • relatedUnits: [order-management]
  • output: flows/submit-order

system:
enabled: true
id: enterprise-system
title: Enterprise System
output: ./enterprise-wiki

  • Optional human-authored facts copied into the aggregation workspace.

    factsPath: ./facts

Copy link
Copy Markdown
Owner Author

WIKIFORGE_PHASE1_PATCH_PART 06/19

  • if c.Documentation.Catalogs.MaximumRowsPerPage <= 0 {
  •   c.Documentation.Catalogs.MaximumRowsPerPage = d.Documentation.Catalogs.MaximumRowsPerPage
    
  • }
  • if c.Documentation.Evidence.Include == nil {
  •   c.Documentation.Evidence.Include = append([]string(nil), d.Documentation.Evidence.Include...)
    
  • }
  • if c.Documentation.Evidence.Exclude == nil {
  •   c.Documentation.Evidence.Exclude = append([]string(nil), d.Documentation.Evidence.Exclude...)
    
  • }
  • if c.Documentation.Evidence.MaxFileSizeBytes <= 0 {
  •   c.Documentation.Evidence.MaxFileSizeBytes = d.Documentation.Evidence.MaxFileSizeBytes
    
  • }
    if c.Mermaid.Mode == "" {
    c.Mermaid.Mode = d.Mermaid.Mode
    }
    @@ -298,13 +445,14 @@ func applyDefaults(c *Config) {
    }

func Validate(c Config) error {

  • if c.Version != 1 && c.Version != CurrentVersion {
  •   return fmt.Errorf("unsupported config version %d; supported versions are 1 and %d", c.Version, CurrentVersion)
    
  • if c.Version != CurrentVersion {
  •   return fmt.Errorf("configuration was not normalized to version %d", CurrentVersion)
    
    }
    if c.OpenWiki.Command == "" {
    return errors.New("openwiki.command is required")
    }
    allIDs := map[string]bool{}
  • portableComponentIDs := map[string]string{}
    enabledIDs := map[string]bool{}
    workdirs := map[string]string{}
    for _, component := range c.Components {
    @@ -320,7 +468,17 @@ func Validate(c Config) error {
    if allIDs[component.ID] {
    return fmt.Errorf("duplicate component id %q", component.ID)
    }
  •   portableID := strings.ToLower(component.ID)
    
  •   if other, exists := portableComponentIDs[portableID]; exists {
    
  •   	return fmt.Errorf("component ids %q and %q differ only by case and are not portable", other, component.ID)
    
  •   }
    
  •   portableComponentIDs[portableID] = component.ID
      allIDs[component.ID] = true
    
  •   for _, pack := range component.Packs {
    
  •   	if !KnownCapabilityPack(pack) {
    
  •   		return fmt.Errorf("component %q has unsupported capability pack %q", component.ID, pack)
    
  •   	}
    
  •   }
      if !component.Enabled {
      	continue
      }
    

@@ -335,10 +493,11 @@ func Validate(c Config) error {
return fmt.Errorf("component %q scope: %w", component.ID, err)
}
workdir := filepath.Clean(component.WorkDir())

  •   if other, ok := workdirs[workdir]; ok {
    
  •   	return fmt.Errorf("components %q and %q resolve to the same work directory %q", other, component.ID, workdir)
    
  •   workdirKey := strings.ToLower(filepath.ToSlash(workdir))
    
  •   if other, ok := workdirs[workdirKey]; ok {
    
  •   	return fmt.Errorf("components %q and %q resolve to the same portable work directory %q", other, component.ID, workdir)
      }
    
  •   workdirs[workdir] = component.ID
    
  •   workdirs[workdirKey] = component.ID
    
    }
    if len(enabledIDs) == 0 {
    return errors.New("at least one enabled component is required")
    @@ -350,6 +509,78 @@ func Validate(c Config) error {
    }
    }
    }
  • unitIDs := map[string]bool{}
  • portableUnitIDs := map[string]string{}
  • unitsByID := map[string][]string{}
  • outputs := map[string]string{}
  • for _, unit := range c.DocumentationUnits {
  •   if err := pathutil.ValidatePortableSegment(unit.ID); err != nil {
    
  •   	return fmt.Errorf("documentation unit id %q is not portable: %w", unit.ID, err)
    
  •   }
    
  •   key := unit.Component + "/" + unit.ID
    
  •   if unitIDs[key] {
    
  •   	return fmt.Errorf("duplicate documentation unit %q in component %q", unit.ID, unit.Component)
    
  •   }
    
  •   portableKey := strings.ToLower(key)
    
  •   if other, exists := portableUnitIDs[portableKey]; exists {
    
  •   	return fmt.Errorf("documentation units %q and %q differ only by case and are not portable", other, key)
    
  •   }
    
  •   portableUnitIDs[portableKey] = key
    
  •   unitIDs[key] = true
    
  •   unitsByID[unit.ID] = append(unitsByID[unit.ID], key)
    
  •   if !enabledIDs[unit.Component] {
    
  •   	return fmt.Errorf("documentation unit %q references unknown or disabled component %q", unit.ID, unit.Component)
    
  •   }
    
  •   if !knownUnitKinds[unit.Kind] {
    
  •   	return fmt.Errorf("documentation unit %q has unsupported kind %q", unit.ID, unit.Kind)
    
  •   }
    
  •   if !knownCriticalities[unit.Criticality] {
    
  •   	return fmt.Errorf("documentation unit %q has unsupported criticality %q", unit.ID, unit.Criticality)
    
  •   }
    
  •   if unit.Output != "" {
    
  •   	outputKey := strings.ToLower(unit.Component + ":" + unit.Output)
    
  •   	if other, ok := outputs[outputKey]; ok {
    
  •   		return fmt.Errorf("documentation units %q and %q share output %q", other, unit.ID, unit.Output)
    
  •   	}
    
  •   	outputs[outputKey] = unit.ID
    
  •   }
    
  • }
  • for _, unit := range c.DocumentationUnits {
  •   for _, related := range unit.RelatedUnits {
    
  •   	if strings.Contains(related, "/") {
    
  •   		if !unitIDs[related] {
    
  •   			return fmt.Errorf("documentation unit %q relates to unknown qualified unit %q", unit.ID, related)
    
  •   		}
    
  •   		continue
    
  •   	}
    
  •   	if unitIDs[unit.Component+"/"+related] {
    
  •   		continue
    
  •   	}
    
  •   	matches := unitsByID[related]
    
  •   	if len(matches) == 0 {
    
  •   		return fmt.Errorf("documentation unit %q relates to unknown unit %q", unit.ID, related)
    
  •   	}
    
  •   	if len(matches) > 1 {
    
  •   		return fmt.Errorf("documentation unit %q has ambiguous relation %q; use component/unit", unit.ID, related)
    
  •   	}
    
  •   }
    
  • }
  • for _, view := range c.Documentation.Views {
  •   if !knownViews[view] {
    
  •   	return fmt.Errorf("unsupported documentation view %q", view)
    
  •   }
    
  • }
  • for _, dimension := range c.Documentation.Catalogs.ShardBy {
  •   if !knownShardDimensions[dimension] {
    
  •   	return fmt.Errorf("unsupported catalog shard dimension %q", dimension)
    
  •   }
    
  • }
  • if c.Documentation.Catalogs.MaximumRowsPerPage < 1 {
  •   return errors.New("documentation.catalogs.maximumRowsPerPage must be positive")
    
  • }
  • if c.Documentation.Evidence.MaxFileSizeBytes < 1 {
  •   return errors.New("documentation.evidence.maxFileSizeBytes must be positive")
    
  • }
    if c.System.Enabled && c.System.Output == "" {
    return errors.New("system.output is required when system.enabled is true")
    }
    @@ -359,10 +590,7 @@ func Validate(c Config) error {
    return nil
    }

-func validateScope(scope string) error {

  • _, err := pathutil.NormalizeRelative(scope)
  • return err
    -}
    +func validateScope(scope string) error { _, err := pathutil.NormalizeRelative(scope); return err }

func (c ComponentConfig) WorkDir() string {
if c.Scope == "" {
@@ -370,11 +598,7 @@ func (c ComponentConfig) WorkDir() string {
}
return filepath.Clean(filepath.Join(c.Repository, c.Scope))
}

-func (c ComponentConfig) DocumentationRoot() string {

  • return filepath.Join(c.WorkDir(), "openwiki")
    -}

+func (c ComponentConfig) DocumentationRoot() string { return filepath.Join(c.WorkDir(), "openwiki") }
func (c ComponentConfig) IsIncludedInSystem() bool {
return c.IncludeInSystem == nil || *c.IncludeInSystem
}
@@ -390,65 +614,104 @@ func (c Config) EnabledComponents() []ComponentConfig {
return out
}

+func (c Config) UnitsForComponent(componentID string) []DocumentationUnitConfig {

  • var out []DocumentationUnitConfig
  • for _, unit := range c.DocumentationUnits {
  •   if unit.Component == componentID {
    
  •   	out = append(out, unit)
    
  •   }
    
  • }
  • sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID })
  • return out
    +}

+func (c Config) ViewEnabled(view string) bool {

  • view = normalizeID(view)
  • for _, candidate := range c.Documentation.Views {
  •   if candidate == view {
    
  •   	return true
    
  •   }
    
  • }
  • return false
    +}

+func (c Config) NormalizedJSON() ([]byte, error) { return c.NormalizedJSONRelativeTo("") }
+
+func (c Config) NormalizedJSONRelativeTo(base string) ([]byte, error) {

  • clone := c
  • clone.SourceVersion = 0
  • clone.Services = nil
  • clone.Execution.ParallelServices = 0
  • clone.Execution.ContinueOnServiceFailure = false
  • if base != "" {
  •   absBase, err := filepath.Abs(base)
    
  •   if err != nil {
    
  •   	return nil, err
    
  •   }
    
  •   rel := func(value string) string {
    
  •   	if value == "" {
    
  •   		return ""
    
  •   	}
    
  •   	candidate, err := filepath.Rel(absBase, value)
    
  •   	if err != nil {
    
  •   		return filepath.ToSlash(value)
    
  •   	}
    
  •   	if candidate == "" {
    
  •   		candidate = "."
    
  •   	}
    
  •   	return filepath.ToSlash(candidate)
    
  •   }
    
  •   clone.Workspace = rel(clone.Workspace)
    
  •   for i := range clone.Components {
    
  •   	clone.Components[i].Repository = rel(clone.Components[i].Repository)
    
  •   	clone.Components[i].Scope = filepath.ToSlash(clone.Components[i].Scope)
    
  •   }
    
  •   for i := range clone.DocumentationUnits {
    
  •   	clone.DocumentationUnits[i].Output = filepath.ToSlash(clone.DocumentationUnits[i].Output)
    
  •   	for j := range clone.DocumentationUnits[i].SourceRoots {
    
  •   		clone.DocumentationUnits[i].SourceRoots[j] = filepath.ToSlash(clone.DocumentationUnits[i].SourceRoots[j])
    
  •   	}
    
  •   }
    
  •   clone.System.Output = rel(clone.System.Output)
    
  •   clone.System.FactsPath = rel(clone.System.FactsPath)
    
  • }
  • return json.MarshalIndent(clone, "", " ")
    +}

var typeToProfile = map[string]string{

  • "generic": "generic",
  • "repository": "generic",
  • "application": "application",
  • "monolith": "application",
  • "microservice": "application",
  • "service": "application",
  • "worker": "application",
  • "gateway": "application",
  • "frontend": "application",
  • "cli": "application",
  • "modular-monolith": "modular-application",
  • "library": "reusable",
  • "shared-library": "reusable",
  • "internal-library": "reusable",
  • "framework": "reusable",
  • "sdk": "reusable",
  • "iac": "infrastructure",
  • "infrastructure": "infrastructure",
  • "gitops": "infrastructure",
  • "platform": "infrastructure",
  • "deployment": "infrastructure",
  • "configuration": "configuration",
  • "shared-config": "configuration",
  • "config": "configuration",
  • "contract": "contracts",
  • "contracts": "contracts",
  • "schema": "contracts",
  • "schemas": "contracts",
    -}

-func normalizeType(value string) string {

  • "generic": "generic", "repository": "generic", "application": "application", "monolith": "application",
  • "microservice": "application", "service": "application", "worker": "application", "gateway": "application",
  • "frontend": "application", "cli": "application", "modular-monolith": "modular-application",
  • "library": "reusable", "shared-library": "reusable", "internal-library": "reusable", "framework": "reusable", "sdk": "reusable",
  • "iac": "infrastructure", "infrastructure": "infrastructure", "gitops": "infrastructure", "platform": "infrastructure", "deployment": "infrastructure",
  • "configuration": "configuration", "shared-config": "configuration", "config": "configuration",
  • "contract": "contracts", "contracts": "contracts", "schema": "contracts", "schemas": "contracts",
    +}

+func normalizeType(value string) string { return normalizeID(value) }
+func normalizeID(value string) string {
value = strings.TrimSpace(strings.ToLower(value))
value = strings.ReplaceAll(value, "_", "-")
value = strings.ReplaceAll(value, " ", "-")
return value
}

func ProfileForType(componentType string) string {
if p, ok := typeToProfile[normalizeType(componentType)]; ok {
return p
}
return "generic"
}

func KnownType(componentType string) bool {
_, ok := typeToProfile[normalizeType(componentType)]
return ok
}

func KnownProfile(profile string) bool {

  • switch strings.TrimSpace(strings.ToLower(profile)) {
  • switch normalizeID(profile) {
    case "application", "modular-application", "reusable", "infrastructure", "configuration", "contracts", "generic":
    return true
  • default:
  •   return false
    
    }
  • return false
    }

func SupportedTypes() []string {
out := make([]string, 0, len(typeToProfile))
for t := range typeToProfile {
@@ -457,3 +720,29 @@ func SupportedTypes() []string {
sort.Strings(out)
return out
}
+func SupportedCapabilityPacks() []string { return append([]string(nil), capabilityPacks...) }
+func KnownCapabilityPack(pack string) bool {

  • pack = normalizeID(pack)
  • i := sort.SearchStrings(capabilityPacks, pack)
  • return i < len(capabilityPacks) && capabilityPacks[i] == pack
    +}
    +func DefaultPacksForProfile(profile string) []string {

@fajarnugraha37 fajarnugraha37 changed the title Phase 1: adaptive documentation planning foundation Phase 1: add adaptive documentation planning foundation Jul 17, 2026
@fajarnugraha37
fajarnugraha37 marked this pull request as ready for review July 17, 2026 21:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant