Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/target
.idea
.claude
.ai-factory
.ai-factory.json
Cargo.lock
.DS_Store
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ $ hlv check
| **Test specs** | `derived_from` refs, unique IDs, gate coverage |
| **Traceability** | REQ → CTR → TST → GATE chains, no dangling refs |
| **Plan** | DAG without cycles, contract coverage |
| **Code traceability** | `@hlv` markers in code match contract rules |
| **Code traceability** | `@hlv` markers in code match contract rules (skipped when `features.hlv_markers: false`) |
| **LLM map** | every `map.yaml` entry exists on disk |
| **Constraints** | rule IDs, severity validation |

Expand All @@ -79,7 +79,7 @@ Phase-aware: checks expected at the current phase are automatically downgraded t

| Command | What it does |
|---------|-------------|
| `hlv init` | Scaffold the full HLV directory structure in seconds |
| `hlv init` | Scaffold the full HLV directory structure (asks for feature flags) |
| `hlv check` | Run the full validation suite — specs, gates, deps, coverage |
| `hlv milestone` | Track progress across milestones |
| `hlv workflow` | See where you are and what the next step is |
Expand Down Expand Up @@ -140,6 +140,18 @@ Each layer catches a different class of drift:

> The compiler doesn't care that the LLM was pretty sure. Neither does hlv.

## Feature flags

HLV ships with opinionated defaults, but you can opt out via `project.yaml`:

```yaml
features:
linear_architecture: true # flat module structure, no layered arch
hlv_markers: true # @hlv code traceability markers + CTR-010/CTR-001
```

Both default to `true`. Set to `false` to use your preferred architecture style or skip `@hlv` markers entirely. `hlv init` asks about these during project setup.

## Your best practices are LLM anti-patterns

| Developer pattern | LLM problem | HLV alternative |
Expand Down
2 changes: 1 addition & 1 deletion docs/STRUCTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Canonical structure created by `hlv init` + `hlv milestone new`.
Source of truth for fixture validation and legacy detection.

```
project.yaml # entry point - status, paths, stack, constraints
project.yaml # entry point - status, paths, stack, constraints, features
milestones.yaml # current milestone, stages, history
HLV.md # methodology rules (auto-generated)
AGENTS.md # project-specific rules (user-editable)
Expand Down
7 changes: 6 additions & 1 deletion docs/WORKFLOW.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,15 @@ hlv init -> hlv milestone new "feature"

```bash
hlv init --project payments --owner backend-team
# init asks for the gate profile (minimal/standard/full) and the first milestone name
# init asks for:
# - gate profile (minimal/standard/full)
# - first milestone name
# - feature flags: linear architecture (Y/n), @hlv markers (Y/n)
# --profile minimal|standard|full can be passed explicitly
```

Feature flags (`features.linear_architecture`, `features.hlv_markers`) control whether HLV's opinionated code style and `@hlv` marker system are enforced. Both default to `true`. Set to `false` in `project.yaml` to opt out.

### 2. Fill in the context

```bash
Expand Down
20 changes: 20 additions & 0 deletions schema/project-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@
},
"git": {
"$ref": "#/$defs/GitPolicy"
},
"features": {
"$ref": "#/$defs/Features"
}
},
"additionalProperties": false,
Expand Down Expand Up @@ -223,6 +226,23 @@
},
"additionalProperties": false
},
"Features": {
"type": "object",
"description": "Optional feature flags controlling HLV's opinionated conventions",
"properties": {
"linear_architecture": {
"type": "boolean",
"default": true,
"description": "Enable linear architecture style in skills"
},
"hlv_markers": {
"type": "boolean",
"default": true,
"description": "Enable @hlv code traceability markers and CTR-010/CTR-001 checks"
}
},
"additionalProperties": false
},
"GitPolicy": {
"type": "object",
"required": ["commit_convention", "merge_strategy"],
Expand Down
20 changes: 17 additions & 3 deletions skills/implement/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,19 @@ metadata:

Execute the implementation plan: agents perform tasks from milestone stage files in parallel, generating code and tests from contracts.

## Step 0: Read Configuration

Before proceeding, read `project.yaml → features` and note the flag values:
- `features.linear_architecture` (default: `true`)
- `features.hlv_markers` (default: `true`)

These flags control which sections below are active. If `project.yaml` has no `features` section, treat both as `true`.

## CRITICAL: Code Architecture Philosophy

> **Conditional: `features.linear_architecture: true`**
> If `linear_architecture` is `false` in project.yaml, skip this entire section. Use your preferred architecture style instead.

> **The human DOES NOT read the generated code. The code is written FOR machines — LLM agents read it, LLM agents modify it, automated gates validate it.**

This changes everything about how code is structured:
Expand Down Expand Up @@ -136,13 +147,13 @@ Each agent when executing a task:
- Test spec (`{MID}/test-specs/<contract>.md`)
- Dependent code (output of previous tasks)
4. **Generate (linear, inline, TDD)**:
- **Code structure**: write linearly — input → validation → logic → output → errors. No layers (controller/service/repository). One file per logical unit. File names are arbitrary (e.g., `01.rs`, `create.rs`) — describe each file in `llm/map.yaml`.
- **Code structure** *(when `features.linear_architecture: true`)*: write linearly — input → validation → logic → output → errors. No layers (controller/service/repository). One file per logical unit. File names are arbitrary (e.g., `01.rs`, `create.rs`) — describe each file in `llm/map.yaml`. *(When `false`: use your preferred architecture style — layered, hexagonal, etc.)*
- **Tests inline**: unit tests go in the same file as code (`#[cfg(test)] mod tests`). Separate `tests/` only for integration tests.
- **`@ctx` comments**: add LLM navigation markers — `// @ctx: stock check for order.create`. Not human docs, but LLM orientation.
- **Tests first**: write unit tests from contract test spec and property-based tests from invariants BEFORE implementation code. Tests must compile (with stubs/unimplemented markers) and clearly fail.
- **Then implement**: write implementation code to make the failing tests pass. No layered abstractions — write the simplest linear code.
- **Then implement**: write implementation code to make the failing tests pass. *(When `features.linear_architecture: true`)* No layered abstractions — write the simplest linear code.
- **Then refine**: once tests are green, refactor if needed while keeping tests green. Duplication across features is OK — don't extract until it hurts.
- **`@hlv` markers (MANDATORY)**: every test MUST carry an `@hlv <ID>` comment linking it to a contract validation or constraint. See "Code Traceability Markers" below.
- **`@hlv` markers** *(when `features.hlv_markers: true`, MANDATORY)*: every test MUST carry an `@hlv <ID>` comment linking it to a contract validation or constraint. See "Code Traceability Markers" below. *(When `false`: skip `@hlv` markers entirely.)*
5. **Validate locally**:
- `cargo check` / `npm run build` / equivalent
- Unit tests pass
Expand Down Expand Up @@ -293,6 +304,9 @@ milestones.yaml # updated stage status

## Code Traceability Markers (`@hlv`)

> **Conditional: `features.hlv_markers: true`**
> If `hlv_markers` is `false` in project.yaml, skip this entire section. No `@hlv` markers are required and `hlv check` will not run CTR-010/CTR-001 checks.

Every contract validation and constraint rule MUST be traceable to test code. `hlv check` enforces this automatically.

### What gets tracked
Expand Down
10 changes: 10 additions & 0 deletions skills/validate/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ metadata:

Execute all mandatory validation gates defined in `gates-policy.yaml`. Collect results, update project status, produce release decision. The gate set depends on the project profile — do NOT assume a fixed number of gates.

## Step 0: Read Configuration

Before proceeding, read `project.yaml → features` and note the flag values:
- `features.hlv_markers` (default: `true`)

This flag controls whether marker-related validation (Step 3b) is active. If `project.yaml` has no `features` section, treat as `true`.

## Prerequisites

- All tasks in current stage completed
Expand Down Expand Up @@ -135,6 +142,9 @@ gate_results:

### Step 3b: Constraint rule coverage

> **Conditional: `features.hlv_markers: true`**
> If `hlv_markers` is `false` in project.yaml, skip the `@hlv` marker check below. `hlv check` will not produce CTR-010 diagnostics. Still run `check_command`-based rules (CST-050/CST-060) as those are independent of markers.

Check that every rule in rule-based constraint files (`human/constraints/*.yaml`) has a corresponding `@hlv <rule-id>` marker in `llm/src/` or `tests/`. Rules with `check_command` are exempt — they are verified programmatically. Run `hlv check` and review CTR-010 diagnostics for missing constraint markers.

`hlv check` also executes `check_command` for rules that define one (CST-050/CST-060). Review diagnostics: rules with `error_level: error` (or `critical`/`high` severity without an override) block release. Add failing checks to the remediation plan (Step 4a).
Expand Down
40 changes: 34 additions & 6 deletions src/check/code_trace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@ pub fn check_code_trace(
constraints: &[ConstraintEntry],
src_path: &str,
tests_path: Option<&str>,
markers_enabled: bool,
) -> Vec<Diagnostic> {
if !markers_enabled {
tracing::debug!("Skipping code trace check — hlv_markers disabled");
return Vec::new();
}

let mut diags = Vec::new();

// 1. Collect expected markers from contracts
Expand Down Expand Up @@ -204,13 +210,35 @@ def test_sql():
assert!(markers.contains("prepared_statements_only"));
}

#[test]
fn check_code_trace_disabled_returns_empty() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
fs::create_dir_all(root.join("llm/src")).unwrap();

// Even with contracts that would produce diagnostics, disabled markers = empty
let contracts = vec![ContractEntry {
id: "test.contract".to_string(),
version: "1.0.0".to_string(),
path: "contracts/test.md".to_string(),
yaml_path: Some("contracts/test.yaml".to_string()),
owner: None,
status: crate::model::project::ContractStatus::Implemented,
test_spec: None,
depends_on: vec![],
artifacts: vec![],
}];
let diags = check_code_trace(root, &contracts, &[], "llm/src", None, false);
assert!(diags.is_empty(), "markers disabled = no diagnostics");
}

#[test]
fn check_code_trace_empty_contracts() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
fs::create_dir_all(root.join("llm/src")).unwrap();

let diags = check_code_trace(root, &[], &[], "llm/src", None);
let diags = check_code_trace(root, &[], &[], "llm/src", None, true);
assert!(diags.is_empty(), "no contracts = no expected markers");
}

Expand Down Expand Up @@ -267,7 +295,7 @@ fn test_atomicity() {}
artifacts: vec![],
}];

let diags = check_code_trace(root, &contracts, &[], "llm/src", None);
let diags = check_code_trace(root, &contracts, &[], "llm/src", None, true);

// Should have CTR-001 summary but no CTR-010 warnings
assert!(
Expand Down Expand Up @@ -319,7 +347,7 @@ outputs_schema:
artifacts: vec![],
}];

let diags = check_code_trace(root, &contracts, &[], "llm/src", None);
let diags = check_code_trace(root, &contracts, &[], "llm/src", None, true);
let warnings: Vec<_> = diags.iter().filter(|d| d.code == "CTR-010").collect();
assert_eq!(warnings.len(), 2, "2 missing markers: {:?}", warnings);
assert!(diags.iter().any(|d| d.code == "CTR-001"));
Expand Down Expand Up @@ -361,7 +389,7 @@ rules:
applies_to: Some("all".to_string()),
}];

let diags = check_code_trace(root, &[], &constraints, "llm/src", None);
let diags = check_code_trace(root, &[], &constraints, "llm/src", None, true);
// One marker found, one missing
let warnings: Vec<_> = diags.iter().filter(|d| d.code == "CTR-010").collect();
assert_eq!(
Expand Down Expand Up @@ -416,7 +444,7 @@ output:
artifacts: vec![],
}];

let diags = check_code_trace(root, &contracts, &[], "llm/src", Some("llm/tests"));
let diags = check_code_trace(root, &contracts, &[], "llm/src", Some("llm/tests"), true);
assert!(
!diags.iter().any(|d| d.code == "CTR-010"),
"marker found in tests dir should count: {:?}",
Expand All @@ -443,7 +471,7 @@ output:
artifacts: vec![],
}];

let diags = check_code_trace(root, &contracts, &[], "llm/src", None);
let diags = check_code_trace(root, &contracts, &[], "llm/src", None, true);
assert!(diags.is_empty(), "no yaml = no expected markers");
}
}
2 changes: 2 additions & 0 deletions src/cmd/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ pub fn get_check_diagnostics(root: &Path) -> Result<(Vec<Diagnostic>, i32)> {
&project.constraints,
&project.paths.llm.src,
tests_path,
project.features.hlv_markers,
));
}

Expand Down Expand Up @@ -330,6 +331,7 @@ fn run_checks(root: &Path) -> Result<i32> {
&project.constraints,
&project.paths.llm.src,
tests_path,
project.features.hlv_markers,
);
print_diags(&code_diags);
all_diags.extend(code_diags);
Expand Down
44 changes: 42 additions & 2 deletions src/cmd/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,15 @@ pub fn run_with_milestone(
None => prompt_with_default("First milestone name", "init")?,
};

let linear_arch = prompt_yes_no("Enable linear architecture style?", true)?;
let hlv_markers = prompt_yes_no("Enable @hlv code traceability markers?", true)?;

tracing::debug!(
linear_architecture = linear_arch,
hlv_markers = hlv_markers,
"Feature flags selected"
);

let agent_dir = format!(".{agent_name}");
let skills_dir = format!("{agent_dir}/skills");

Expand Down Expand Up @@ -287,7 +296,7 @@ pub fn run_with_milestone(
write_template(
root,
"project.yaml",
&project_template(&project_name, &owner_name),
&project_template(&project_name, &owner_name, linear_arch, hlv_markers),
)?;
write_template(root, "milestones.yaml", &milestones_template(&project_name))?;
write_template(
Expand Down Expand Up @@ -351,6 +360,28 @@ fn prompt_with_default(label: &str, default: &str) -> Result<String> {
}
}

/// Prompt user for a yes/no answer with a default.
fn prompt_yes_no(label: &str, default: bool) -> Result<bool> {
let hint = if default { "Y/n" } else { "y/N" };
print!(" {} {} [{}]: ", "?".cyan().bold(), label, hint);
io::stdout().flush()?;
let mut line = String::new();
io::stdin()
.lock()
.read_line(&mut line)
.context("failed to read input")?;
let value = line.trim().to_lowercase();
if value.is_empty() {
Ok(default)
} else {
match value.as_str() {
"y" | "yes" => Ok(true),
"n" | "no" => Ok(false),
_ => anyhow::bail!("Expected y/n, got '{}'", line.trim()),
}
}
}

/// Detect agent name from existing `.{agent}/skills/` directory.
fn detect_agent(root: &Path) -> Result<String> {
for entry in fs::read_dir(root)? {
Expand Down Expand Up @@ -635,7 +666,12 @@ exceptions:
)
}

fn project_template(project: &str, _owner: &str) -> String {
fn project_template(
project: &str,
_owner: &str,
linear_architecture: bool,
hlv_markers: bool,
) -> String {
format!(
r#"# yaml-language-server: $schema=schema/project-schema.json
# HLV Project Map
Expand Down Expand Up @@ -670,6 +706,10 @@ constraints:
path: human/constraints/observability.yaml
applies_to: all

features:
linear_architecture: {linear_architecture}
hlv_markers: {hlv_markers}

git:
branch_per_milestone: false
commit_convention: conventional
Expand Down
Loading
Loading