diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 65ed0a91..d8d5bcd1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -193,6 +193,33 @@ SpecFact CLI uses **Spec-Driven Development (SDD)** via [OpenSpec](./openspec/) - Test documentation examples - **Update OpenSpec specs**: When implementing features, ensure [`openspec/specs/`](./openspec/specs/) reflects the new behavior +### Entry-Point Messaging Hierarchy + +The repository README, `docs/index.md`, and other first-contact surfaces must preserve the same +first-contact story. + +When editing those surfaces, make sure a new visitor can quickly answer: +- **What is SpecFact?** +- **Why does it exist?** +- **Why should I use it?** +- **What do I get?** +- **How do I get started?** + +Keep the hierarchy in this order: +1. Product identity +2. Why it exists +3. User value +4. How to get started +5. Deeper topology and cross-site handoff + +For first-contact pages, define SpecFact as the validation and alignment layer for software delivery +and present “keep backlog, specs, tests, and code in sync” as the user-visible outcome of that +positioning. + +GitHub-facing repo metadata must reinforce the same story. Keep the repository description, topics, +and other above-the-fold cues aligned with the README hero so visitors see the same product +identity before and after opening the repository. + ### Documentation Structure - `README.md`: Project overview and quick start diff --git a/README.md b/README.md index aa7cd631..8b4653e9 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ # SpecFact CLI -> **The "swiss knife" CLI that turns any codebase into a clear, safe, and shippable workflow.** -> Keep backlog, specs, tests, and code in sync so changes made by people or AI copilots do not break production. -> Works for brand-new projects and long-lived codebases - even if you are new to coding. +> **SpecFact is the validation and alignment layer for software delivery.** +> It adds the missing validation layer that keeps backlog intent, specifications, tests, and code +> from drifting apart across AI-assisted coding, brownfield systems, and governed delivery. +> Use it to move fast without losing rigor. **No API keys required. Works offline. Zero vendor lock-in.** @@ -23,21 +24,54 @@ invoke SpecFact as part of a command chain. --- -## Documentation Topology +## What is SpecFact? -`docs.specfact.io` is the canonical docs entry point for SpecFact. +SpecFact is the validation and alignment layer for software delivery. -- Core CLI/runtime/platform documentation remains owned by `specfact-cli`. -- Module-specific deep docs are canonically owned by `specfact-cli-modules`. -- The live modules docs site is currently published at `https://modules.specfact.io/`. +It is a local CLI that helps you keep the intent behind a change aligned from +backlog or idea through specifications, implementation, and checks. The “Swiss-knife CLI” metaphor +fits because SpecFact gives you a set of focused tools for specific delivery problems, not a vague +bag of features. -Use this repository's docs for the overall SpecFact workflow, CLI runtime lifecycle, module registry, trust model, and command-group topology. -Use the modules docs site for bundle-specific deep dives, adapter details, workflow tutorials, and module-authoring guidance. -In short, module-specific deep docs are canonically owned by `specfact-cli-modules`. +In practice, SpecFact helps you: +- add guardrails to AI-assisted and fast-moving greenfield work +- reverse-engineer large brownfield codebases into trustworthy structured understanding +- reduce the “I wanted X but got Y” drift between backlog, spec, and implementation +- move from local rigor toward team and enterprise policy enforcement ---- +## Why does it exist? + +Modern delivery drifts in predictable ways: +- AI-generated quick wins often lack the validation layer needed for mid- and long-term reliability +- brownfield systems often have missing or drifted specs, so teams need to reverse-engineer reality +- backlog intent gets reinterpreted into something else before it reaches code +- teams working with different skill levels, opinions, and AI IDE setups need consistent review and + policy enforcement + +SpecFact exists to reduce that drift. It helps teams understand what is really there, express what +should happen more accurately, and validate that the result still matches the original intent. + +## Why should I use it? + +Use SpecFact if you want one of these outcomes: +- ship AI-assisted changes faster without accepting fragile “looks fine to me” quality +- understand a legacy or unfamiliar codebase before changing it +- hand brownfield insight into OpenSpec, Spec-Kit, or other spec-first workflows +- keep backlog expectations, specifications, and implementation from silently diverging +- enforce shared rules consistently across developers and CI/CD -## Start Here (60 seconds) +## What do I get? + +With SpecFact, you get: +- a deterministic local CLI instead of another opaque SaaS dependency +- a validation layer around fast-moving implementation work +- codebase analysis and sidecar flows for brownfield understanding +- stronger backlog/spec/code alignment for real delivery workflows +- a path from individual rigor to organization-level policy enforcement + +## How do I get started? + +### Start Here (5 minutes) ### Install @@ -49,38 +83,104 @@ uvx specfact-cli@latest pip install -U specfact-cli ``` -### Bootstrap and IDE Setup +### Bootstrap ```bash -# First run: install official bundles +# Recommended first run specfact init --profile solo-developer - -# Alternative bundle selection -specfact init --install backlog,codebase -specfact init --install all - -# IDE prompt/template setup -specfact init ide -specfact init ide --ide cursor -specfact init ide --ide vscode ``` -`specfact init ide` discovers prompt resources from installed workflow modules and exports them to your IDE. If module prompt payloads are not installed yet, the CLI uses packaged fallback resources. - -### Run Your First Flow +### Get First Value ```bash -# Analyze an existing codebase +# Analyze a codebase you care about specfact code import my-project --repo . -# Snapshot current project state +# Snapshot the project state for follow-up workflows specfact project snapshot --bundle my-project -# Validate external code without modifying source +# Validate external code without modifying the target repo specfact code validate sidecar init my-project /path/to/repo specfact code validate sidecar run my-project /path/to/repo ``` +That path gives you a concrete first win: SpecFact understands your project context and gives you a +validated starting point instead of jumping straight into blind change work. + +### AI IDE Setup + +```bash +specfact init ide +specfact init ide --ide cursor +specfact init ide --ide vscode +``` + +`specfact init ide` discovers prompt resources from installed workflow modules and exports them to +your IDE. If module prompt payloads are not installed yet, the CLI uses packaged fallback resources. + +## Choose Your Path + +### Greenfield and AI-assisted delivery + +Use SpecFact as the validation layer around fast-moving implementation work. + +Start with: +- `specfact init --profile solo-developer` +- `specfact code validate sidecar init /path/to/repo` +- `specfact code validate sidecar run /path/to/repo` + +### Brownfield and reverse engineering + +Use SpecFact to understand an existing system before you change it, then hand that understanding +into spec-first tools such as OpenSpec or Spec-Kit. + +Start with: +- `specfact code import my-project --repo .` +- `specfact project snapshot --bundle my-project` +- `specfact code validate sidecar init my-project /path/to/repo` +- `specfact code validate sidecar run my-project /path/to/repo` + +### Backlog to code alignment + +Use SpecFact when the problem is not only code quality, but drift between expectations and delivery. +Backlog commands require a backlog-enabled profile or installed backlog bundle before the workflow +commands are available. + +Start with: +- `specfact init --profile backlog-team` +- `specfact backlog ceremony standup ...` +- `specfact backlog ceremony refinement ...` +- `specfact backlog verify-readiness --bundle ` + +### Team and policy enforcement + +Use SpecFact when multiple developers and AI IDEs need consistent checks and review behavior. + +Start with: +- `specfact backlog verify-readiness --bundle ` +- `specfact govern ...` +- CI validation flows that keep the same rules active outside local development + +## How do I get started if I want more? + +**Next steps** + +- **[Core CLI docs](docs/index.md)** for the core runtime, bootstrap, validation, and command topology +- **[Reference: command topology](docs/reference/commands.md)** for grouped command surfaces +- **[Canonical modules docs site](https://modules.specfact.io/)** for bundle-deep workflows + +## Documentation Topology + +`docs.specfact.io` is the canonical starting point for SpecFact. + +- Core CLI/runtime/platform documentation remains owned by `specfact-cli`. +- Module-specific deep docs are canonically owned by `specfact-cli-modules`. +- The live modules docs site is published at `https://modules.specfact.io/`. + +Use this repository's docs for the overall product story, runtime lifecycle, command topology, +trust model, and getting-started flow. Use the modules docs site when you want deeper workflow, +adapter, and module-authoring guidance. + ### Migration Note (Flat Commands Removed) As of `0.40.0`, flat root commands are removed. Use grouped commands: @@ -120,12 +220,6 @@ For GitHub, replace adapter/org/project with: /specfact.01-import my-project --repo . ``` -**Next steps** - -- **[Core CLI docs](docs/index.md)** -- **[Reference: command topology](docs/reference/commands.md)** -- **[Canonical modules docs site](https://modules.specfact.io/)** - --- ## Who It Is For diff --git a/docs/README.md b/docs/README.md index 8770f112..f0bd4b8c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,10 +8,16 @@ description: High-level index for the SpecFact core CLI docs and canonical modul # SpecFact CLI Documentation This repository owns the **core CLI** documentation set for SpecFact. -It explains the overall process of using SpecFact CLI, the platform runtime, and how official modules integrate into the grouped command surface. +Use it as the canonical starting point when a user still needs orientation around what SpecFact is, +why it exists, what value it provides, and how to get started. -For **module-specific deep functionality**, use the canonical modules docs site at `https://modules.specfact.io/`. -The canonical modules docs site owns the detailed guides for bundle workflows, adapters, and module authoring. +SpecFact is the validation and alignment layer for software delivery. The core docs explain the +product story, runtime lifecycle, bootstrap path, and the handoff into deeper module-owned +workflows. + +For **module-specific deep functionality**, use the canonical modules docs site at +`https://modules.specfact.io/`. The canonical modules docs site owns the detailed guides for bundle +workflows, adapters, and module authoring. ## Core Docs Scope @@ -35,6 +41,10 @@ Use the canonical modules docs site for: The canonical modules docs site is currently published at `https://modules.specfact.io/`. This docs set keeps release-line overview and handoff content for bundle workflows while the canonical modules docs site carries the deep bundle-specific guidance. +If a modules page is ever used as a first-contact surface, it must explain that `modules.specfact.io` +is the deeper workflow layer and direct un-oriented users back to `docs.specfact.io` for the core +product story and fast-start path. + ## Cross-site contract - [Documentation URL contract (core and modules)](reference/documentation-url-contract.md) — linking rules vs `modules.specfact.io` diff --git a/docs/index.md b/docs/index.md index 0895533e..9a46c7e7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,7 +1,7 @@ --- layout: default title: SpecFact CLI Documentation -description: Core CLI docs for runtime lifecycle, command topology, and official module integration. +description: SpecFact is the validation and alignment layer for software delivery. Start here for the core CLI story, first steps, and the handoff into module-deep workflows. permalink: / keywords: [specfact, core-cli, runtime, module-system, architecture] audience: [solo, team, enterprise] @@ -17,57 +17,101 @@ exempt_reason: "" # SpecFact CLI Documentation -SpecFact CLI is a contract-first Python CLI that keeps backlogs, specs, tests, and code in sync. This site covers the core platform - runtime, lifecycle, command topology, and architecture. +SpecFact is the validation and alignment layer for software delivery. -SpecFact CLI does **not** include built-in AI. It pairs deterministic CLI commands with slash-command prompts in your chosen IDE. +This site is the canonical starting point for the core CLI story: what SpecFact is, why it exists, +what value you get from it, how to get started, and when to move into deeper bundle-owned workflows. -For module-specific workflows (backlog, governance, adapters), see [modules.specfact.io](https://modules.specfact.io/). - -Use the shared portal navigation to move between **Docs Home**, **Core CLI**, and **Modules** without changing interaction patterns. +SpecFact does **not** include built-in AI. It pairs deterministic CLI commands with your chosen IDE +and copilot so fast-moving work has a stronger validation and alignment layer around it. --- -## Find Your Path +## What is SpecFact? + +SpecFact helps you keep backlog intent, specifications, implementation, and validation from drifting +apart. + +It is especially useful when: +- AI-assisted or “vibe-coded” work needs more rigor +- brownfield systems need trustworthy reverse-engineered understanding +- teams want to avoid the “I wanted X but got Y” delivery failure +- organizations need a path toward stronger shared policy enforcement + +## Why does it exist? + +Software delivery drifts in stages. Expectations change as they move from backlog language to +specification, from specification to implementation, and from implementation to review. SpecFact +exists to reduce that drift by giving you deterministic tooling for analysis, validation, and +alignment. + +## Why should I use it? + +Use SpecFact when you want faster delivery without losing validation, stronger brownfield +understanding before making changes, and less drift between backlog intent, specifications, and the +code that actually lands. + +## What do I get? + +With SpecFact, you get: +- deterministic local tooling instead of opaque cloud dependence +- a validation layer around AI-assisted delivery +- codebase analysis and sidecar validation for brownfield work +- stronger backlog/spec/code alignment +- a clean handoff from core runtime docs into module-deep workflows on `modules.specfact.io` + +## How to get started + +1. **[Installation](/getting-started/installation/)** - Install SpecFact CLI +2. **[5-Minute Quickstart](/getting-started/quickstart/)** - Get first value quickly +3. **[specfact init](/core-cli/init/)** - Bootstrap the core runtime and your local setup +4. **[Bootstrap Checklist](/module-system/bootstrap-checklist/)** - Verify bundle readiness + +If you are new to SpecFact, start here before jumping into module-deep workflows. + +## Choose Your Path
-

New User

-

Start with the core install and bootstrap path before adding workflow bundles.

+

Greenfield & AI-assisted delivery

+

Use SpecFact as the validation layer around fast-moving implementation work.

-

Team Lead

-

Set up shared runtime conventions, IDE flows, and team-level operating guidance.

+

Brownfield and reverse engineering

+

Use SpecFact to understand an existing system and then hand insight into spec-first workflows.

-

Platform Owner

-

Use the architecture and registry references to operate SpecFact as shared platform infrastructure.

+

Backlog to code alignment

+

Use SpecFact when the main problem is drift between expectations, specs, and implementation.

-

Module Operator

-

Manage installed bundles from core docs, then hand off to modules docs for bundle-owned workflows.

+

Team and policy enforcement

+

Use core runtime, governance, and shared workflow conventions to scale rigor across teams.

@@ -83,11 +127,14 @@ The `specfact-cli` package provides the stable platform surface: Installed modules mount workflows under `project`, `backlog`, `code`, `spec`, and `govern`. -## Get Started +## Modules Documentation -1. **[Installation](/getting-started/installation/)** - Install SpecFact CLI -2. **[5-Minute Quickstart](/getting-started/quickstart/)** - First analysis in under 5 minutes -3. **[Bootstrap Checklist](/module-system/bootstrap-checklist/)** - Verify modules are installed +`docs.specfact.io` is the default starting point. Move to the modules site when you need deeper +bundle-specific workflows, adapters, and authoring guidance. + +- **[Modules Docs Home](https://modules.specfact.io/)** - Backlog, project, spec, govern +- **[Module Development](https://modules.specfact.io/authoring/module-development/)** - Build your own modules +- **[Publishing Modules](https://modules.specfact.io/authoring/publishing-modules/)** - Publish to marketplace ## Module System @@ -123,11 +170,3 @@ Installed modules mount workflows under `project`, `backlog`, `code`, `spec`, an - **[Migration Guide](/migration/migration-guide/)** - Version upgrade guidance - **[CLI Reorganization](/migration/migration-cli-reorganization/)** - Command surface changes - **[OpenSpec Migration](/migration/openspec-migration/)** - OPSX workflow migration - -## Modules Documentation - -For in-depth module workflows, visit the canonical modules site: - -- **[Modules Docs Home](https://modules.specfact.io/)** - Backlog, project, spec, govern -- **[Module Development](https://modules.specfact.io/authoring/module-development/)** - Build your own modules -- **[Publishing Modules](https://modules.specfact.io/authoring/publishing-modules/)** - Publish to marketplace diff --git a/docs/reference/documentation-url-contract.md b/docs/reference/documentation-url-contract.md index fbc82bf7..64f24863 100644 --- a/docs/reference/documentation-url-contract.md +++ b/docs/reference/documentation-url-contract.md @@ -32,6 +32,16 @@ The **authoritative** URL and ownership rules for **both** documentation sites a 2. **Handoff pages** (see OpenSpec `docs-07-core-handoff-conversion`) must point to the **modules canonical URL** for each topic, with a short summary and prerequisites on core. 3. **Internal core links** must continue to resolve on `docs.specfact.io` per published `permalink` (docs review gate / parity tests). +## First-contact handoff contract + +- `docs.specfact.io` is the default starting point for first-time users. +- `modules.specfact.io` is the deeper workflow and bundle documentation layer. +- Core docs must explain what extra value the modules docs provide before linking users there. +- Modules landing pages must send un-oriented users back to the core docs when they still need the + product story, first-run guidance, or overall navigation context. +- Both sites must preserve the same product identity: SpecFact as the validation and alignment layer + for software delivery. + ## Repositories | Concern | Repository | diff --git a/openspec/CHANGE_ORDER.md b/openspec/CHANGE_ORDER.md index ef3e8a18..30d61f21 100644 --- a/openspec/CHANGE_ORDER.md +++ b/openspec/CHANGE_ORDER.md @@ -118,7 +118,7 @@ The 2026-03-22 clean-code plan adds one new cross-repo change pair and re-sequen | docs | 07 | docs-07-core-handoff-conversion | [#439](https://github.com/nold-ai/specfact-cli/issues/439) | docs-05-core-site-ia-restructure; modules-repo/docs-06-modules-site-ia-restructure | | docs | 08 | docs-12-docs-validation-ci | [#440](https://github.com/nold-ai/specfact-cli/issues/440) | docs-05-core-site-ia-restructure; docs-07-core-handoff-conversion; modules-repo/docs-06 through docs-10 | | docs | 09 | docs-13-core-nav-search-theme-roles | [#458](https://github.com/nold-ai/specfact-cli/issues/458) | docs-05-core-site-ia-restructure; docs-07-core-handoff-conversion; docs-12-docs-validation-ci; modules-repo/docs-13-nav-search-theme-roles (design parity only, no content ownership coupling) | -| docs | 10 | docs-14-first-contact-story-and-onboarding | [#466](https://github.com/nold-ai/specfact-cli/issues/466) | docs-05-core-site-ia-restructure ✅; docs-07-core-handoff-conversion ✅; docs-12-docs-validation-ci ✅; docs-13-core-nav-search-theme-roles ✅; Parent Feature: [#356](https://github.com/nold-ai/specfact-cli/issues/356) | +| docs | 10 | docs-14-first-contact-story-and-onboarding (in progress) | [#466](https://github.com/nold-ai/specfact-cli/issues/466) | docs-05-core-site-ia-restructure ✅; docs-07-core-handoff-conversion ✅; docs-12-docs-validation-ci ✅; docs-13-core-nav-search-theme-roles ✅; Parent Feature: [#356](https://github.com/nold-ai/specfact-cli/issues/356) | ### Docs refactoring plan addendum (2026-03-23) diff --git a/openspec/changes/docs-14-first-contact-story-and-onboarding/TDD_EVIDENCE.md b/openspec/changes/docs-14-first-contact-story-and-onboarding/TDD_EVIDENCE.md new file mode 100644 index 00000000..40472792 --- /dev/null +++ b/openspec/changes/docs-14-first-contact-story-and-onboarding/TDD_EVIDENCE.md @@ -0,0 +1,60 @@ +# TDD Evidence for docs-14-first-contact-story-and-onboarding + +## Pre-Implementation Test Failure (Expected) + +### Test Run: 2026-03-30 - First-contact story contract + +**Command:** + +```bash +cd /home/dom/git/nold-ai/specfact-cli-worktrees/feature/docs-14-first-contact-story-and-onboarding +python3 -m pytest tests/unit/docs/test_first_contact_story.py -q +``` + +**Result:** ✅ 5 tests failed as expected before the README/docs/contributor rewrite. + +**Failure summary:** + +- `README.md` did not yet define SpecFact as the validation and alignment layer. +- The README still placed documentation topology before the primary start path. +- The README did not yet provide explicit “choose your path” outcome routing. +- `docs/index.md` did not yet mirror the new first-contact story structure. +- `CONTRIBUTING.md` did not yet document the first-contact hierarchy and required questions. + +**Status:** ✅ Failing-first evidence captured before implementation. + +## Post-Implementation Passing Validation + +### Test Run: 2026-03-30 - First-contact story and docs handoff + +**Commands:** + +```bash +cd /home/dom/git/nold-ai/specfact-cli-worktrees/feature/docs-14-first-contact-story-and-onboarding +hatch env create +hatch run format +hatch run type-check +hatch run lint +hatch run contract-test +hatch test --cover -v +python3 -m pytest tests/unit/docs/test_first_contact_story.py tests/unit/test_core_docs_site_contract.py tests/unit/docs/test_release_docs_parity.py -q -k 'first_contact_story or core_landing_page_marks_core_repo_as_canonical_owner or readme_and_docs_index_define_core_and_modules_split' +hatch run yaml-lint +openspec validate docs-14-first-contact-story-and-onboarding --strict +``` + +**Result:** ✅ The full documented quality-gate sequence passed for this change. `hatch run type-check` +completed with `0 errors` and existing repo-wide test warnings outside this change scope; `hatch run +lint`, `hatch run contract-test`, and `hatch test --cover -v` all completed successfully. + +**Passing summary:** + +- `README.md` now answers the five first-contact questions in order and leads with the validation and + alignment story. +- `docs/index.md` mirrors the same story and keeps the core-to-modules handoff explicit. +- `docs/README.md` and `docs/reference/documentation-url-contract.md` now document the intended + `docs.specfact.io` to `modules.specfact.io` onboarding split. +- `CONTRIBUTING.md` now records the entry-point messaging hierarchy and repo-metadata alignment rule. +- The audited `hatch run type-check` gate was executed and recorded for this change; warnings came + from pre-existing repo-wide test typing debt rather than the touched first-contact files. +- The remaining local quality gates required by the checklist (`lint`, `contract-test`, and full + covered tests) were executed and passed before the follow-up review fixes were finalized. diff --git a/openspec/changes/docs-14-first-contact-story-and-onboarding/tasks.md b/openspec/changes/docs-14-first-contact-story-and-onboarding/tasks.md index 74e1119d..04c39dcf 100644 --- a/openspec/changes/docs-14-first-contact-story-and-onboarding/tasks.md +++ b/openspec/changes/docs-14-first-contact-story-and-onboarding/tasks.md @@ -10,72 +10,79 @@ evidence in `TDD_EVIDENCE.md`. ## 1. Create git worktree for this change -- [ ] 1.1 Fetch latest and create a worktree with a new branch from `origin/dev`. -- [ ] 1.2 Change into the worktree and run `hatch env create`. -- [ ] 1.3 Verify the branch name and working directory match `docs-14-first-contact-story-and-onboarding`. +- [x] 1.1 Fetch latest and create a worktree with a new branch from `origin/dev`. +- [x] 1.2 Change into the worktree and run `hatch env create`. +- [x] 1.3 Verify the branch name and working directory match `docs-14-first-contact-story-and-onboarding`. +- [x] 1.4 Run `hatch run smart-test-status` from inside the worktree. +- [x] 1.5 Run `hatch run contract-test-status` from inside the worktree. ## 2. Research and message contract -- [ ] 2.1 Review the current first-contact surfaces: GitHub repo landing, `README.md`, `docs/index.md`, +- [x] 2.1 Review the current first-contact surfaces: GitHub repo landing, `README.md`, `docs/index.md`, and the current modules-site homepage/handoff copy. -- [ ] 2.2 Capture the current answers to: - - [ ] 2.2.1 What is SpecFact? - - [ ] 2.2.2 Why does it exist? - - [ ] 2.2.3 Why should I use it? - - [ ] 2.2.4 What do I get from it? - - [ ] 2.2.5 How do I get started? -- [ ] 2.3 Define the canonical story hierarchy and the one recommended fast-start path before editing +- [x] 2.2 Capture the current answers to: + - [x] 2.2.1 What is SpecFact? + - [x] 2.2.2 Why does it exist? + - [x] 2.2.3 Why should I use it? + - [x] 2.2.4 What do I get from it? + - [x] 2.2.5 How do I get started? +- [x] 2.3 Define the canonical story hierarchy and the one recommended fast-start path before editing implementation files. -- [ ] 2.4 Lock the sharper USP in writing: - - [ ] 2.4.1 SpecFact as the validation and alignment layer for software delivery - - [ ] 2.4.2 AI-assisted greenfield validation as one entry value path - - [ ] 2.4.3 Brownfield reverse-engineering into spec-first workflows as another value path - - [ ] 2.4.4 backlog-to-code drift reduction as the end-to-end business value - - [ ] 2.4.5 enterprise policy management as the scale-up story, not the only audience +- [x] 2.4 Lock the sharper USP in writing: + - [x] 2.4.1 SpecFact as the validation and alignment layer for software delivery + - [x] 2.4.2 AI-assisted greenfield validation as one entry value path + - [x] 2.4.3 Brownfield reverse-engineering into spec-first workflows as another value path + - [x] 2.4.4 backlog-to-code drift reduction as the end-to-end business value + - [x] 2.4.5 enterprise policy management as the scale-up story, not the only audience ## 3. Test-first / evidence-first preparation -- [ ] 3.1 Add or update docs validation checks, snapshot-style assertions, or reviewable evidence that +- [x] 3.1 Add or update docs validation checks, snapshot-style assertions, or reviewable evidence that prove the new messaging hierarchy and first-run path are present. -- [ ] 3.2 Record the pre-implementation state in `TDD_EVIDENCE.md`, including the current README/docs +- [x] 3.2 Record the pre-implementation state in `TDD_EVIDENCE.md`, including the current README/docs wording and any failing or missing validation checks. ## 4. Implementation: GitHub and README entry point -- [ ] 4.1 Rewrite the top of `README.md` around the canonical identity statement, value proposition, +- [x] 4.1 Rewrite the top of `README.md` around the canonical identity statement, value proposition, and one fast-start path. -- [ ] 4.2 Add explicit “choose your path” guidance after the primary getting-started flow. -- [ ] 4.3 Document the intended GitHub repository description/topics/tagline updates in the repo-owned +- [x] 4.2 Add explicit “choose your path” guidance after the primary getting-started flow. +- [x] 4.3 Document the intended GitHub repository description/topics/tagline updates in the repo-owned source so maintainers can apply the same story above the fold. -- [ ] 4.4 Ensure the README answers the five first-contact questions explicitly and in order. +- [x] 4.4 Ensure the README answers the five first-contact questions explicitly and in order. ## 5. Implementation: Core docs and modules handoff -- [ ] 5.1 Update `docs/index.md` and any adjacent landing/navigation copy so `docs.specfact.io` +- [x] 5.1 Update `docs/index.md` and any adjacent landing/navigation copy so `docs.specfact.io` mirrors the same story and onboarding order as the README. -- [ ] 5.2 Add or update the core-docs handoff to `modules.specfact.io` so it explains why and when a +- [x] 5.2 Add or update the core-docs handoff to `modules.specfact.io` so it explains why and when a user should move to module-deep docs. -- [ ] 5.3 Define the required modules-homepage wording/contract for the paired `specfact-cli-modules` +- [x] 5.3 Define the required modules-homepage wording/contract for the paired `specfact-cli-modules` implementation so the modules site routes un-oriented users back to core docs. -- [ ] 5.4 Make the brownfield/spec-first handoff explicit in core and modules onboarding copy. +- [x] 5.4 Make the brownfield/spec-first handoff explicit in core and modules onboarding copy. ## 6. Implementation: Alignment and contributor guidance -- [ ] 6.1 Update contributor-facing guidance so future entry-point edits preserve the same messaging +- [x] 6.1 Update contributor-facing guidance so future entry-point edits preserve the same messaging hierarchy. -- [ ] 6.2 Ensure cross-site ownership wording remains consistent with the current core-versus-modules +- [x] 6.2 Ensure cross-site ownership wording remains consistent with the current core-versus-modules documentation contract. ## 7. Validation and quality gates -- [ ] 7.1 `hatch run format` -- [ ] 7.2 `hatch run yaml-lint` -- [ ] 7.3 Run the targeted docs/tests/review checks added for this change. -- [ ] 7.4 Update `TDD_EVIDENCE.md` with post-implementation passing evidence and before/after entry-point comparisons. -- [ ] 7.5 Run `openspec validate docs-14-first-contact-story-and-onboarding --strict`. +- [x] 7.1 `hatch run format` +- [x] 7.2 `hatch run type-check` +- [x] 7.3 `hatch run lint` +- [x] 7.4 `hatch run contract-test` +- [x] 7.5 `hatch test --cover -v` +- [x] 7.6 `hatch run yaml-lint` +- [x] 7.7 Run the targeted docs/tests/review checks added for this change. +- [x] 7.8 Update `TDD_EVIDENCE.md` with post-implementation passing evidence and before/after entry-point comparisons. +- [x] 7.9 Run `openspec validate docs-14-first-contact-story-and-onboarding --strict`. ## 8. Delivery -- [ ] 8.1 Update `openspec/CHANGE_ORDER.md` with implementation status when work begins/lands. -- [ ] 8.2 Stage and commit with a Conventional Commit message. -- [ ] 8.3 Push the feature branch and open a PR to `dev`. +- [x] 8.1 Update `openspec/CHANGE_ORDER.md` with implementation status when work begins/lands. +- [x] 8.2 Stage and commit with a Conventional Commit message. +- [x] 8.3 Push the feature branch and open a PR to `dev`. +- [ ] 8.4 After merge to `dev`, remove the worktree and delete the feature branch locally/remotely. diff --git a/tests/unit/docs/test_first_contact_story.py b/tests/unit/docs/test_first_contact_story.py new file mode 100644 index 00000000..4a12a170 --- /dev/null +++ b/tests/unit/docs/test_first_contact_story.py @@ -0,0 +1,115 @@ +"""Validate first-contact messaging across the core repo entry points. + +These tests ensure the README, docs landing page, and contributor guidance all +present the same canonical product story and onboarding order. +""" + +import re +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[3] +README = REPO_ROOT / "README.md" +DOCS_INDEX = REPO_ROOT / "docs" / "index.md" +CONTRIBUTING = REPO_ROOT / "CONTRIBUTING.md" + +assert REPO_ROOT.exists(), f"Repository root missing: expected at {REPO_ROOT}" +assert README.is_file(), f"README.md missing: expected at {README}" +assert DOCS_INDEX.is_file(), f"docs/index.md missing: expected at {DOCS_INDEX}" +assert CONTRIBUTING.is_file(), f"CONTRIBUTING.md missing: expected at {CONTRIBUTING}" + + +def _read(path: Path) -> str: + """Return the UTF-8 text contents of a repository file. + + Args: + path: Path to the file to read. + + Returns: + File contents as a string. + """ + + return path.read_text(encoding="utf-8") + + +def _assert_question_order(content: str, questions: list[str], surface: str) -> None: + """Assert that the first-contact questions appear in increasing order. + + Args: + content: Text content to inspect. + questions: Ordered question strings that must appear in sequence. + surface: Human-readable name of the file or surface being inspected. + """ + + indices: list[int] = [] + for question in questions: + index = content.find(question) + assert index != -1, f"Missing question {question!r} in {surface}" + indices.append(index) + + assert indices == sorted(indices), f"Questions are out of order in {surface}: {questions}" + + +def test_readme_leads_with_validation_and_alignment_story() -> None: + readme = _read(README) + questions = [ + "What is SpecFact?", + "Why does it exist?", + "Why should I use it?", + "What do I get?", + "How do I get started?", + ] + + assert "validation and alignment layer" in readme + _assert_question_order(readme, questions, "README.md") + + +def test_readme_prioritizes_fast_start_over_docs_topology() -> None: + readme = _read(README) + + start_match = re.search(r"^#+\s*Start Here", readme, re.MULTILINE) + topology_match = re.search(r"^#+\s*Documentation Topology", readme, re.MULTILINE) + assert start_match is not None, "Missing Start Here heading in README.md" + assert topology_match is not None, "Missing Documentation Topology heading in README.md" + start_idx = start_match.start() + topology_idx = topology_match.start() + assert start_idx < topology_idx + + +def test_readme_routes_users_by_outcome() -> None: + readme = _read(README) + + assert "## Choose Your Path" in readme + assert "Greenfield and AI-assisted delivery" in readme + assert "Brownfield and reverse engineering" in readme + assert "Backlog to code alignment" in readme + + +def test_docs_index_matches_first_contact_story() -> None: + docs_index = _read(DOCS_INDEX) + questions = [ + "What is SpecFact?", + "Why does it exist?", + "Why should I use it?", + "What do I get?", + "How to get started", + ] + + assert "validation and alignment layer" in docs_index + _assert_question_order(docs_index, questions, "docs/index.md") + assert "modules.specfact.io" in docs_index + assert "brownfield" in docs_index.lower() + + +def test_contributing_guidance_mentions_entrypoint_story_hierarchy() -> None: + contributing = _read(CONTRIBUTING) + questions = [ + "What is SpecFact?", + "Why does it exist?", + "Why should I use it?", + "What do I get?", + "How do I get started?", + ] + + assert "first-contact" in contributing + _assert_question_order(contributing, questions, "CONTRIBUTING.md") diff --git a/tests/unit/docs/test_release_docs_parity.py b/tests/unit/docs/test_release_docs_parity.py index 90f14ce0..794ca5b5 100644 --- a/tests/unit/docs/test_release_docs_parity.py +++ b/tests/unit/docs/test_release_docs_parity.py @@ -2,7 +2,7 @@ import re from pathlib import Path -from urllib.parse import unquote, urlparse +from urllib.parse import ParseResult, unquote, urlparse import yaml @@ -127,91 +127,138 @@ def _is_published_docs_route_candidate(route: str) -> bool: return route not in {"/assets/main.css/", "/feed.xml/"} -def _resolve_internal_docs_target( +def _missing_route_failure(source: Path, route: str) -> tuple[str, None, str]: + return route, None, f"{source.relative_to(_repo_root())} -> {route}" + + +def _resolve_site_token_link(source: Path, stripped: str) -> tuple[str | None, str | None]: + if "{{" not in stripped or "site." not in stripped: + return stripped, None + + match = re.search(r"\{\{\s*site\.([A-Za-z0-9_]+)(?:\s*\|.*?)*\s*\}\}", stripped) + if not match: + return None, f"{source.relative_to(_repo_root())} -> unresolved site token {stripped}" + + key = match.group(1) + config = _docs_config() + value = config.get(key) + if not isinstance(value, str) or not value.strip(): + return None, f"{source.relative_to(_repo_root())} -> docs/_config.yml missing non-empty site.{key}" + if key.endswith("_url") and not value.startswith("http"): + return None, f"{source.relative_to(_repo_root())} -> docs/_config.yml site.{key} must start with http" + + suffix = stripped[match.end() :] + return value.strip() + suffix, None + + +def _resolve_published_route( source: Path, - raw_link: str, + route: str, route_to_path: dict[str, Path], - path_to_route: dict[Path, str], ) -> tuple[str | None, Path | None, str | None]: - stripped = _normalize_jekyll_relative_url(raw_link.strip()) - if not stripped or stripped.startswith("#"): + if not _is_published_docs_route_candidate(route): return None, None, None - if "{{" in stripped and "site." in stripped: - match = re.search(r"site\.([a-zA-Z0-9_]+)", stripped) - if not match: - return None, None, f"{source.relative_to(_repo_root())} -> unresolved site token {stripped}" - key = match.group(1) - config = _docs_config() - value = config.get(key) - if not isinstance(value, str) or not value.strip(): - return None, None, f"{source.relative_to(_repo_root())} -> docs/_config.yml missing non-empty site.{key}" - if key.endswith("_url") and not value.startswith("http"): - return None, None, f"{source.relative_to(_repo_root())} -> docs/_config.yml site.{key} must start with http" - stripped = value.strip() - parsed = urlparse(stripped) - if parsed.scheme in {"mailto", "javascript", "tel"}: - return None, None, None - if parsed.scheme in {"http", "https"}: - if parsed.netloc != DOCS_HOST: - return None, None, None - route = _normalize_route(parsed.path or "/") - if not _is_published_docs_route_candidate(route): - return None, None, None - target = route_to_path.get(route) - if target is None: - return route, None, f"{source.relative_to(_repo_root())} -> {route}" - return route, target, None - if parsed.scheme: - return None, None, None + target = route_to_path.get(route) + if target is None: + return _missing_route_failure(source, route) + return route, target, None - target_value = unquote(parsed.path) - if not target_value: + +def _resolve_http_docs_link( + source: Path, + parsed: ParseResult, + route_to_path: dict[str, Path], +) -> tuple[str | None, Path | None, str | None]: + if parsed.netloc != DOCS_HOST: return None, None, None + return _resolve_published_route(source, _normalize_route(parsed.path or "/"), route_to_path) + + +def _resolve_absolute_docs_link( + source: Path, + target_value: str, + route_to_path: dict[str, Path], +) -> tuple[str | None, Path | None, str | None]: + return _resolve_published_route(source, _normalize_route(target_value), route_to_path) + + +def _resolve_existing_candidate( + source: Path, + target_value: str, + candidate: Path, + path_to_route: dict[Path, str], +) -> tuple[str | None, Path | None, str | None]: + route = path_to_route.get(candidate) + if route is None: + return None, None, f"{source.relative_to(_repo_root())} -> {target_value}" + return route, candidate, None - if target_value.startswith("/"): - route = _normalize_route(target_value) - if not _is_published_docs_route_candidate(route): - return None, None, None - target = route_to_path.get(route) - if target is None: - return route, None, f"{source.relative_to(_repo_root())} -> {route}" - return route, target, None +def _resolve_relative_docs_link( + source: Path, + target_value: str, + route_to_path: dict[str, Path], + path_to_route: dict[Path, str], +) -> tuple[str | None, Path | None, str | None]: candidate = (source.parent / target_value).resolve() + if candidate.is_dir(): readme_candidate = (candidate / "README.md").resolve() if readme_candidate.is_file() and _is_docs_markdown(readme_candidate): - route = path_to_route.get(readme_candidate) - if route is None: - return None, None, f"{source.relative_to(_repo_root())} -> {target_value}" - return route, readme_candidate, None + return _resolve_existing_candidate(source, target_value, readme_candidate, path_to_route) return None, None, None if candidate.is_file() and _is_docs_markdown(candidate): - route = path_to_route.get(candidate) - if route is None: - return None, None, f"{source.relative_to(_repo_root())} -> {target_value}" - return route, candidate, None + return _resolve_existing_candidate(source, target_value, candidate, path_to_route) if not candidate.suffix: markdown_candidate = candidate.with_suffix(".md") if markdown_candidate.is_file() and _is_docs_markdown(markdown_candidate): - resolved_candidate = markdown_candidate.resolve() - route = path_to_route.get(resolved_candidate) - if route is None: - return None, None, f"{source.relative_to(_repo_root())} -> {target_value}" - return route, resolved_candidate, None + return _resolve_existing_candidate(source, target_value, markdown_candidate.resolve(), path_to_route) route = _normalize_route(target_value) if not _is_published_docs_route_candidate(route): return None, None, None + target = route_to_path.get(route) if target is None: return route, None, f"{source.relative_to(_repo_root())} -> {target_value} (normalized: {route})" return route, target, None +def _resolve_internal_docs_target( + source: Path, + raw_link: str, + route_to_path: dict[str, Path], + path_to_route: dict[Path, str], +) -> tuple[str | None, Path | None, str | None]: + stripped = _normalize_jekyll_relative_url(raw_link.strip()) + if not stripped or stripped.startswith("#"): + return None, None, None + + stripped, site_token_failure = _resolve_site_token_link(source, stripped) + if site_token_failure is not None or stripped is None: + return None, None, site_token_failure + + parsed = urlparse(stripped) + if parsed.scheme in {"mailto", "javascript", "tel"}: + return None, None, None + if parsed.scheme in {"http", "https"}: + return _resolve_http_docs_link(source, parsed, route_to_path) + if parsed.scheme: + return None, None, None + + target_value = unquote(parsed.path) + if not target_value: + return None, None, None + + if target_value.startswith("/"): + return _resolve_absolute_docs_link(source, target_value, route_to_path) + + return _resolve_relative_docs_link(source, target_value, route_to_path, path_to_route) + + def _navigation_sources() -> list[Path]: return [ _repo_file("docs/index.md").resolve(), @@ -294,12 +341,13 @@ def test_module_contracts_reference_external_bundle_boundary() -> None: def test_readme_and_docs_index_define_core_and_modules_split() -> None: readme = _repo_file("README.md").read_text(encoding="utf-8") docs_index = _repo_file("docs/index.md").read_text(encoding="utf-8") - assert "canonical docs entry point" in readme - assert "module-specific deep docs are canonically owned by `specfact-cli-modules`" in readme + assert "validation and alignment layer for software delivery" in readme + assert "docs.specfact.io` is the canonical starting point for SpecFact" in readme + assert "Module-specific deep docs are canonically owned by `specfact-cli-modules`" in readme _assert_mentions_modules_docs_site(readme) - assert "Docs Home" in docs_index - assert "Core CLI" in docs_index - assert "Modules" in docs_index + assert "canonical starting point for the core CLI story" in docs_index + assert "docs.specfact.io` is the default starting point" in docs_index + assert "modules.specfact.io" in docs_index def test_top_navigation_exposes_docs_home_core_cli_and_modules() -> None: @@ -345,31 +393,36 @@ def _scan_authored_docs(pattern: str) -> list[tuple[str, int, str]]: """ hits: list[tuple[str, int, str]] = [] repo_root = _repo_root() - sources: list[Path] = [repo_root / "README.md"] - docs_dir = repo_root / "docs" - for path in docs_dir.rglob("*.md"): - if "_site" not in path.parts and "vendor" not in path.parts: - sources.append(path) - for src in sources: + for src in _authored_doc_sources(repo_root): if not src.exists(): continue for lineno, line in enumerate(src.read_text(encoding="utf-8").splitlines(), 1): if pattern not in line: continue stripped = line.strip() - if stripped.startswith("#") and not stripped.startswith( - ("# ", "## ", "### ", "#### ", "##### ", "###### ") - ): - continue - if stripped.startswith(">"): - continue - lower = stripped.lower() - if "removed" in lower or "(removed)" in lower or "is removed" in lower: + if _skip_historical_pattern_hit(stripped): continue hits.append((str(src.relative_to(repo_root)), lineno, stripped)) return hits +def _authored_doc_sources(repo_root: Path) -> list[Path]: + sources: list[Path] = [repo_root / "README.md"] + docs_dir = repo_root / "docs" + sources.extend(path for path in docs_dir.rglob("*.md") if "_site" not in path.parts and "vendor" not in path.parts) + return sources + + +def _skip_historical_pattern_hit(stripped: str) -> bool: + if stripped.startswith("#") and not stripped.startswith(("# ", "## ", "### ", "#### ", "##### ", "###### ")): + return True + if stripped.startswith(">"): + return True + + lower = stripped.lower() + return "removed" in lower or "(removed)" in lower or "is removed" in lower + + def _fmt_hits(hits: list[tuple[str, int, str]]) -> str: return "\n".join(f" {path}:{lineno} {line}" for path, lineno, line in hits) diff --git a/tests/unit/test_core_docs_site_contract.py b/tests/unit/test_core_docs_site_contract.py index 89daf7aa..ec9d9462 100644 --- a/tests/unit/test_core_docs_site_contract.py +++ b/tests/unit/test_core_docs_site_contract.py @@ -1,6 +1,4 @@ -from html.parser import HTMLParser from pathlib import Path -from urllib.parse import urlparse import yaml @@ -17,19 +15,6 @@ ) -class _HrefParser(HTMLParser): - def __init__(self) -> None: - super().__init__() - self.hrefs: list[str] = [] - - def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: - if tag != "a": - return - for key, value in attrs: - if key == "href" and value is not None: - self.hrefs.append(value) - - def _read(path: Path) -> str: try: return path.read_text(encoding="utf-8") @@ -41,20 +26,6 @@ def _config() -> dict[str, object]: return yaml.safe_load(_read(DOCS_CONFIG)) -def _index_hrefs() -> list[str]: - parser = _HrefParser() - parser.feed(_read(DOCS_INDEX)) - return parser.hrefs - - -def _has_modules_docs_home_link(hrefs: list[str]) -> bool: - for href in hrefs: - parsed = urlparse(href) - if parsed.scheme == "https" and parsed.netloc == "modules.specfact.io" and parsed.path == "/": - return True - return False - - def test_core_docs_config_targets_public_core_domain() -> None: config = _config() @@ -67,12 +38,11 @@ def test_core_docs_config_targets_public_core_domain() -> None: def test_core_landing_page_marks_core_repo_as_canonical_owner() -> None: index = _read(DOCS_INDEX) - hrefs = _index_hrefs() - assert "This site covers the core platform" in index - assert "module-specific workflows" in index - assert "shared portal navigation" in index - assert _has_modules_docs_home_link(hrefs) + assert "SpecFact is the validation and alignment layer for software delivery." in index + assert "canonical starting point for the core CLI story" in index + assert "module-deep workflows" in index + assert "https://modules.specfact.io/" in index assert "nold-ai.github.io/specfact-cli" not in index