Skip to content
Draft
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
1 change: 1 addition & 0 deletions .hyperloop/checks/check-no-api-simulation.sh
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ check_dir() {
--include="*.js" \
--exclude="*.test.*" \
--exclude="*.spec.*" \
--exclude-dir=.venv \
-E "new Promise[^)]*setTimeout[[:space:]]*\([[:space:]]*resolve" \
"$dir" 2>/dev/null || true)

Expand Down
60 changes: 51 additions & 9 deletions .hyperloop/checks/check-process-agent-not-on-task-branch.sh
Original file line number Diff line number Diff line change
@@ -1,27 +1,34 @@
#!/usr/bin/env bash
# check-process-agent-not-on-task-branch.sh
#
# Fails if the current branch matches the task-branch pattern hyperloop/task-NNN.
# Guards against process-improvement commits landing on hyperloop/task-NNN branches.
#
# PURPOSE: Pre-commit gate for the process-improvement agent. Process-improvement
# commits must NEVER land on hyperloop/task-NNN branches — they must go to a
# dedicated process-improvement branch (e.g. branched from alpha).
# PURPOSE: This script has two distinct modes of operation:
#
# PRE-COMMIT MODE (staged files exist):
# The process-improvement agent is about to commit. Fail immediately if
# the current branch is a task branch — the commit must not proceed.
#
# VERIFICATION MODE (no staged files):
# The orchestrator is validating a task branch. Pass if no commits with
# Task-Ref: process-improvement exist in the branch history. Fail only
# if such commits are actually present (the bad outcome already occurred).
#
# WHY: When process-improvement commits land on a task branch, they carry
# "Task-Ref: process-improvement" trailers that cause check-no-foreign-task-commits.sh
# to fail for the task. Observed in task-019: two process-improvement agent commits
# on the task branch caused a verifier FAIL that required orchestrator intervention.
#
# CORRECT FIX (if you find yourself on a task branch):
# CORRECT FIX (if you find yourself on a task branch about to commit):
# 1. Switch to a process-improvement branch:
# git checkout -b process-improvement/$(date +%Y%m%d) origin/alpha
# 2. Cherry-pick your uncommitted work, or push to the correct branch.
#
# Usage:
# bash .hyperloop/checks/check-process-agent-not-on-task-branch.sh
#
# Exit 0 — current branch is not a task branch; safe to commit.
# Exit 1 — current branch is a task branch; commit is blocked.
# Exit 0 — safe (not a task branch, OR task branch with no PI commits present).
# Exit 1 — unsafe (about to commit PI work on a task branch, or PI commits exist).

set -uo pipefail

Expand All @@ -32,7 +39,17 @@ if [[ -z "$BRANCH" || "$BRANCH" == "HEAD" ]]; then
exit 0
fi

if echo "$BRANCH" | grep -qE '^hyperloop/task-[0-9]+'; then
if ! echo "$BRANCH" | grep -qE '^hyperloop/task-[0-9]+'; then
echo "PASS: Current branch ($BRANCH) is not a hyperloop/task-NNN branch — safe to commit."
exit 0
fi

# We are on a task branch. Distinguish mode by checking for staged files.
staged_count=$(git diff --cached --name-only 2>/dev/null | wc -l || echo "0")
staged_count="${staged_count//[[:space:]]/}"

if [[ "$staged_count" -gt 0 ]]; then
# PRE-COMMIT MODE: a commit is about to be made — block it.
echo ""
echo "FAIL: Current branch is a task branch: $BRANCH"
echo ""
Expand All @@ -54,5 +71,30 @@ if echo "$BRANCH" | grep -qE '^hyperloop/task-[0-9]+'; then
exit 1
fi

echo "PASS: Current branch ($BRANCH) is not a hyperloop/task-NNN branch — safe to commit."
# VERIFICATION MODE: no staged files — check whether PI commits are present.
MERGE_BASE=$(git merge-base HEAD alpha 2>/dev/null || true)
if [[ -z "$MERGE_BASE" ]]; then
echo "INFO: No common ancestor with 'alpha' — cannot scan for PI commits."
echo "PASS: Skipping PI-commit check (no alpha merge-base)."
exit 0
fi

pi_commit_count=$(git log --format="%B" "${MERGE_BASE}..HEAD" 2>/dev/null \
| grep -c '^Task-Ref: process-improvement' || echo "0")
pi_commit_count="${pi_commit_count//[[:space:]]/}"

if [[ "$pi_commit_count" -gt 0 ]]; then
echo ""
echo "FAIL: Task branch '$BRANCH' contains $pi_commit_count process-improvement commit(s)."
echo ""
echo "These commits carry Task-Ref: process-improvement trailers that will cause"
echo "check-no-foreign-task-commits.sh to fail for this task."
echo ""
echo "Resolution: drop the process-improvement commits via interactive rebase"
echo " and apply them to a dedicated process-improvement branch instead."
echo ""
exit 1
fi

echo "PASS: Task branch '$BRANCH' contains no process-improvement commits — goal achieved."
exit 0
63 changes: 51 additions & 12 deletions .hyperloop/checks/check-process-improvement-commit-is-clean.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@
# This script combines all three invariants into a single pre-commit gate
# so the process-improvement agent cannot forget to check any of them.
#
# VERIFICATION MODE (no staged files, called by orchestrator):
# When no files are staged this script is running as a branch-history check,
# not as an active pre-commit gate. In that context:
# - Condition 1: PASS if no process-improvement commits (Task-Ref: process-improvement)
# exist in the branch history (the goal was achieved).
# - Conditions 2 & 3: trivially PASS (nothing staged).
#
# Usage:
# bash .hyperloop/checks/check-process-improvement-commit-is-clean.sh
#
Expand All @@ -40,18 +47,50 @@ if [[ -z "$BRANCH" || "$BRANCH" == "HEAD" ]]; then
echo " Run: git checkout -b process-improvement/$(date +%Y%m%d-%H%M%S) origin/alpha"
FAIL=1
elif echo "$BRANCH" | grep -qE '^hyperloop/task-[0-9]+'; then
echo ""
echo "FAIL [1/3]: Current branch is a task branch: $BRANCH"
echo ""
echo " Process-improvement commits must NEVER land on hyperloop/task-NNN branches."
echo " They carry Task-Ref: process-improvement trailers that cause"
echo " check-no-foreign-task-commits.sh to FAIL on the task, requiring"
echo " orchestrator-level branch reconstruction to clean up."
echo ""
echo " Fix: git checkout -b process-improvement/$(date +%Y%m%d-%H%M%S) origin/alpha"
echo " Then cherry-pick or redo your changes on the new branch."
echo ""
FAIL=1
# Distinguish pre-commit mode (staged files present) from verification mode.
staged_count=$(git diff --cached --name-only 2>/dev/null | wc -l || echo "0")
staged_count="${staged_count//[[:space:]]/}"

if [[ "$staged_count" -gt 0 ]]; then
# PRE-COMMIT MODE: actively blocking the agent from committing on a task branch.
echo ""
echo "FAIL [1/3]: Current branch is a task branch: $BRANCH"
echo ""
echo " Process-improvement commits must NEVER land on hyperloop/task-NNN branches."
echo " They carry Task-Ref: process-improvement trailers that cause"
echo " check-no-foreign-task-commits.sh to FAIL on the task, requiring"
echo " orchestrator-level branch reconstruction to clean up."
echo ""
echo " Fix: git checkout -b process-improvement/$(date +%Y%m%d-%H%M%S) origin/alpha"
echo " Then cherry-pick or redo your changes on the new branch."
echo ""
FAIL=1
else
# VERIFICATION MODE: no staged files — check branch history for PI commits.
MERGE_BASE=$(git merge-base HEAD alpha 2>/dev/null || true)
if [[ -n "$MERGE_BASE" ]]; then
pi_commit_count=$(git log --format="%B" "${MERGE_BASE}..HEAD" 2>/dev/null \
| grep -c '^Task-Ref: process-improvement' || echo "0")
pi_commit_count="${pi_commit_count//[[:space:]]/}"

if [[ "$pi_commit_count" -gt 0 ]]; then
echo ""
echo "FAIL [1/3]: Task branch '$BRANCH' contains $pi_commit_count process-improvement commit(s)."
echo ""
echo " These commits carry Task-Ref: process-improvement trailers that will cause"
echo " check-no-foreign-task-commits.sh to FAIL for this task."
echo ""
echo " Resolution: drop these commits via interactive rebase and apply them"
echo " to a dedicated process-improvement branch instead."
echo ""
FAIL=1
else
echo "PASS [1/3]: Task branch '$BRANCH' — no process-improvement commits present."
fi
else
echo "PASS [1/3]: Task branch '$BRANCH' — no alpha merge-base found; skipping PI scan."
fi
fi
else
echo "PASS [1/3]: Branch '$BRANCH' is not a task branch."
fi
Expand Down
105 changes: 105 additions & 0 deletions src/api/management/presentation/data_sources/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import json
from datetime import datetime

from pydantic import BaseModel, Field
Expand All @@ -10,6 +11,86 @@
from management.domain.entities import DataSourceSyncRun


class NodeTypeDefinition(BaseModel):
"""A proposed or approved node type in the knowledge graph ontology."""

label: str = Field(..., description="The node type label (e.g., 'Repository')")
description: str = Field(
..., description="Human-readable description of the node type"
)
required_properties: list[str] = Field(
default_factory=list,
description="Properties that must be present on every node of this type",
)
optional_properties: list[str] = Field(
default_factory=list,
description="Properties that may be present on nodes of this type",
)


class EdgeTypeDefinition(BaseModel):
"""A proposed or approved edge type in the knowledge graph ontology."""

label: str = Field(..., description="The edge type label (e.g., 'CONTAINS')")
description: str = Field(
..., description="Human-readable description of the edge type"
)
from_type: str = Field(..., description="The source node type label")
to_type: str = Field(..., description="The target node type label")
required_properties: list[str] = Field(
default_factory=list,
description="Properties that must be present on every edge of this type",
)
optional_properties: list[str] = Field(
default_factory=list,
description="Properties that may be present on edges of this type",
)


class OntologyDefinition(BaseModel):
"""A complete ontology definition with node and edge types."""

node_types: list[NodeTypeDefinition] = Field(
default_factory=list,
description="Node types in the ontology",
)
edge_types: list[EdgeTypeDefinition] = Field(
default_factory=list,
description="Edge types in the ontology",
)


class ProposeOntologyRequest(BaseModel):
"""Request model for proposing an ontology for a data source."""

adapter_type: str = Field(
...,
description="Adapter type (e.g., 'github')",
)
intent: str = Field(
...,
description="Free-text description of what the user wants to learn from this data",
min_length=1,
)
connection_config: dict | None = Field(
default=None,
description="Optional connection configuration for the adapter (used for lightweight scan)",
)


class ProposeOntologyResponse(BaseModel):
"""Response model for a proposed ontology."""

node_types: list[NodeTypeDefinition] = Field(
default_factory=list,
description="Proposed node types based on the adapter and intent",
)
edge_types: list[EdgeTypeDefinition] = Field(
default_factory=list,
description="Proposed edge types based on the adapter and intent",
)


class CreateDataSourceRequest(BaseModel):
"""Request model for creating a data source."""

Expand All @@ -31,6 +112,30 @@ class CreateDataSourceRequest(BaseModel):
default=None,
description="Optional credentials to encrypt and store securely",
)
ontology: OntologyDefinition | None = Field(
default=None,
description=(
"Optional approved ontology (node and edge types) to associate with this "
"data source. When provided, the approved types are stored with the data "
"source configuration and used to guide extraction."
),
)

def build_connection_config_with_ontology(self) -> dict:
"""Return connection_config merged with the approved ontology.

The ontology is stored under the reserved ``_ontology`` key so it
travels with the data source configuration without requiring a
separate database column at this stage.

Returns:
A copy of connection_config with ``_ontology`` injected when
an ontology was provided, or the original dict otherwise.
"""
config = dict(self.connection_config)
if self.ontology is not None:
config["_ontology"] = json.loads(self.ontology.model_dump_json())
return config


class DataSourceResponse(BaseModel):
Expand Down
Loading