Advisory issues read as advice rather than failure
Docs / docs (push) Successful in 21s
Playwright Tests / test-playwright (1, 2) (push) Failing after 2m37s
Playwright Tests / test-playwright (2, 2) (push) Canceled after 35s
Playwright Tests / merge-reports (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Test Backend / test-backend (push) Canceled after 0s
Compose Smoke Test / test-compose (push) Canceled after 0s
Docs / docs (push) Successful in 21s
Playwright Tests / test-playwright (1, 2) (push) Failing after 2m37s
Playwright Tests / test-playwright (2, 2) (push) Canceled after 35s
Playwright Tests / merge-reports (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Test Backend / test-backend (push) Canceled after 0s
Compose Smoke Test / test-compose (push) Canceled after 0s
`ADVISORY_ISSUES` moves next to `ValidationIssue` in pipeline.py, and the model derives an `advisory` flag from its own code, so the distinction the engine already made ships to the client instead of being re-guessed there. The dock keeps its summary in `--destructive` only when a real fault is among the issues and paints an advisory row `--muted-foreground`; the canvas leaves advisories off a node's dot and border entirely, since node status has three colours and no warning tier. biome checks the generated `openapi.json`, which nothing formats since the SDK script dropped its format pass — ignore it like the other generated files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Gf7WaExcJ9bs3kfJXB3nK
This commit is contained in:
@@ -20,7 +20,8 @@ from sqlmodel import col, select
|
|||||||
from fluksio.api.deps import FlowControllerDep, SessionDep, get_current_user
|
from fluksio.api.deps import FlowControllerDep, SessionDep, get_current_user
|
||||||
from fluksio.api.routes.runs import elapsed_ms
|
from fluksio.api.routes.runs import elapsed_ms
|
||||||
from fluksio.core.config import settings
|
from fluksio.core.config import settings
|
||||||
from fluksio.flow.controller import ADVISORY_ISSUES, NodeStatus
|
from fluksio.flow.controller import NodeStatus
|
||||||
|
from fluksio.flow.pipeline import ADVISORY_ISSUES
|
||||||
from fluksio.models import EngineEvent, FlowRun, MetricBucket
|
from fluksio.models import EngineEvent, FlowRun, MetricBucket
|
||||||
|
|
||||||
router = APIRouter(
|
router = APIRouter(
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ from fluksio.flow.nodes import (
|
|||||||
TriggerNode,
|
TriggerNode,
|
||||||
)
|
)
|
||||||
from fluksio.flow.pipeline import (
|
from fluksio.flow.pipeline import (
|
||||||
|
ADVISORY_ISSUES,
|
||||||
NodeOutcome,
|
NodeOutcome,
|
||||||
Pipeline,
|
Pipeline,
|
||||||
RunCacheLookup,
|
RunCacheLookup,
|
||||||
@@ -81,10 +82,6 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
HOOK_PREFIX = "/hooks"
|
HOOK_PREFIX = "/hooks"
|
||||||
|
|
||||||
# Validation codes that are worth saying but do not stop a flow running, so
|
|
||||||
# neither the brain graph nor the health summary treats them as a fault.
|
|
||||||
ADVISORY_ISSUES = frozenset({"unauthenticated_hook"})
|
|
||||||
|
|
||||||
# How long a node gets to close what it opened before the rebuild moves on.
|
# How long a node gets to close what it opened before the rebuild moves on.
|
||||||
# A node's `stop` talks to whatever it connected to, and a broker that has gone
|
# A node's `stop` talks to whatever it connected to, and a broker that has gone
|
||||||
# away can leave it waiting for an acknowledgement that never arrives — which
|
# away can leave it waiting for an acknowledgement that never arrives — which
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ from concurrent.futures import Future, ThreadPoolExecutor, wait
|
|||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from typing import Any, Literal, Protocol
|
from typing import Any, Literal, Protocol
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, computed_field
|
||||||
|
|
||||||
from fluksio.flow import logs
|
from fluksio.flow import logs
|
||||||
from fluksio.flow.artifacts import is_reference
|
from fluksio.flow.artifacts import is_reference
|
||||||
@@ -41,9 +41,15 @@ logger = logging.getLogger(__name__)
|
|||||||
#: manual run shows up in the history — but it is no one's idempotency key.
|
#: manual run shows up in the history — but it is no one's idempotency key.
|
||||||
MANUAL_RUN_PREFIX = "manual-"
|
MANUAL_RUN_PREFIX = "manual-"
|
||||||
|
|
||||||
|
# Validation codes that are worth saying but do not stop a flow running, so
|
||||||
|
# neither the brain graph nor the health summary treats them as a fault.
|
||||||
|
# Lives here rather than beside the controller so `ValidationIssue` can carry
|
||||||
|
# the distinction itself, and every reader gets it for free.
|
||||||
|
ADVISORY_ISSUES = frozenset({"unauthenticated_hook"})
|
||||||
|
|
||||||
|
|
||||||
class ValidationIssue(BaseModel):
|
class ValidationIssue(BaseModel):
|
||||||
"""A problem that keeps a flow from running correctly."""
|
"""Something wrong with a flow — a fault, or merely advisory."""
|
||||||
|
|
||||||
code: Literal[
|
code: Literal[
|
||||||
"cycle",
|
"cycle",
|
||||||
@@ -60,6 +66,12 @@ class ValidationIssue(BaseModel):
|
|||||||
port: str | None = None
|
port: str | None = None
|
||||||
message_name: str | None = None
|
message_name: str | None = None
|
||||||
|
|
||||||
|
@computed_field # type: ignore[prop-decorator]
|
||||||
|
@property
|
||||||
|
def advisory(self) -> bool:
|
||||||
|
"""Worth saying, but not a fault — the UI says so in a softer tone."""
|
||||||
|
return self.code in ADVISORY_ISSUES
|
||||||
|
|
||||||
|
|
||||||
class ValueSource(BaseModel):
|
class ValueSource(BaseModel):
|
||||||
"""Who caused a message to take its current value.
|
"""Who caused a message to take its current value.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from fluksio.flow.controller import _declared_inputs
|
|||||||
from fluksio.flow.events import EventBus
|
from fluksio.flow.events import EventBus
|
||||||
from fluksio.flow.messages import DType, MessageSpec
|
from fluksio.flow.messages import DType, MessageSpec
|
||||||
from fluksio.flow.nodes import Node
|
from fluksio.flow.nodes import Node
|
||||||
from fluksio.flow.pipeline import Pipeline
|
from fluksio.flow.pipeline import Pipeline, ValidationIssue
|
||||||
from fluksio.flow.schemas import FlowDef, FlowInput
|
from fluksio.flow.schemas import FlowDef, FlowInput
|
||||||
|
|
||||||
|
|
||||||
@@ -197,3 +197,12 @@ def test_values_carry_timestamps():
|
|||||||
values = pipeline.values("f")
|
values = pipeline.values("f")
|
||||||
assert values["f.out"]["value"] == 1.0
|
assert values["f.out"]["value"] == 1.0
|
||||||
assert values["f.out"]["ts"] > 0
|
assert values["f.out"]["ts"] > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_only_advisory_codes_are_flagged_advisory():
|
||||||
|
"""The UI reads this to keep an advisory out of the fault tone."""
|
||||||
|
hook = ValidationIssue(code="unauthenticated_hook", message="open")
|
||||||
|
cycle = ValidationIssue(code="cycle", message="loop")
|
||||||
|
|
||||||
|
assert hook.advisory is True
|
||||||
|
assert cycle.advisory is False
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"!**/dist/**/*",
|
"!**/dist/**/*",
|
||||||
"!**/node_modules/**/*",
|
"!**/node_modules/**/*",
|
||||||
"!**/src/routeTree.gen.ts",
|
"!**/src/routeTree.gen.ts",
|
||||||
|
"!**/openapi.json",
|
||||||
"!**/src/client/**/*",
|
"!**/src/client/**/*",
|
||||||
"!**/src/components/ui/**/*",
|
"!**/src/components/ui/**/*",
|
||||||
"!**/playwright-report",
|
"!**/playwright-report",
|
||||||
|
|||||||
@@ -3185,12 +3185,18 @@ export const ValidationIssueSchema = {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
title: 'Message Name'
|
title: 'Message Name'
|
||||||
|
},
|
||||||
|
advisory: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: 'Advisory',
|
||||||
|
description: 'Worth saying, but not a fault — the UI says so in a softer tone.',
|
||||||
|
readOnly: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
type: 'object',
|
type: 'object',
|
||||||
required: ['code', 'message'],
|
required: ['code', 'message', 'advisory'],
|
||||||
title: 'ValidationIssue',
|
title: 'ValidationIssue',
|
||||||
description: 'A problem that keeps a flow from running correctly.'
|
description: 'Something wrong with a flow — a fault, or merely advisory.'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const ValidationResultSchema = {
|
export const ValidationResultSchema = {
|
||||||
|
|||||||
@@ -1057,7 +1057,7 @@ export type ValidationError = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A problem that keeps a flow from running correctly.
|
* Something wrong with a flow — a fault, or merely advisory.
|
||||||
*/
|
*/
|
||||||
export type ValidationIssue = {
|
export type ValidationIssue = {
|
||||||
code: 'cycle' | 'unconnected_input' | 'missing_initial_value' | 'node_error' | 'unauthenticated_hook' | 'self_loop_needs_initial';
|
code: 'cycle' | 'unconnected_input' | 'missing_initial_value' | 'node_error' | 'unauthenticated_hook' | 'self_loop_needs_initial';
|
||||||
@@ -1067,6 +1067,10 @@ export type ValidationIssue = {
|
|||||||
node?: (string | null);
|
node?: (string | null);
|
||||||
port?: (string | null);
|
port?: (string | null);
|
||||||
message_name?: (string | null);
|
message_name?: (string | null);
|
||||||
|
/**
|
||||||
|
* Worth saying, but not a fault — the UI says so in a softer tone.
|
||||||
|
*/
|
||||||
|
readonly advisory: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type code = 'cycle' | 'unconnected_input' | 'missing_initial_value' | 'node_error' | 'unauthenticated_hook' | 'self_loop_needs_initial';
|
export type code = 'cycle' | 'unconnected_input' | 'missing_initial_value' | 'node_error' | 'unauthenticated_hook' | 'self_loop_needs_initial';
|
||||||
|
|||||||
@@ -110,6 +110,10 @@ export function FlowDock({
|
|||||||
}) {
|
}) {
|
||||||
const { fitView } = useReactFlow()
|
const { fitView } = useReactFlow()
|
||||||
const connected = useLiveConnection()
|
const connected = useLiveConnection()
|
||||||
|
// The engine marks the issues it does not count against a flow. They are
|
||||||
|
// still worth saying, so they stay in the list — but in the muted tone, and
|
||||||
|
// without turning the summary red, since nothing here is actually broken.
|
||||||
|
const faults = issues.filter((issue) => !issue.advisory)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -175,7 +179,12 @@ export function FlowDock({
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-11 gap-1.5 text-destructive md:h-8"
|
className={cn(
|
||||||
|
"h-11 gap-1.5 md:h-8",
|
||||||
|
faults.length > 0
|
||||||
|
? "text-destructive"
|
||||||
|
: "text-muted-foreground",
|
||||||
|
)}
|
||||||
data-testid="validation-summary"
|
data-testid="validation-summary"
|
||||||
>
|
>
|
||||||
<AlertCircle className="size-4" />
|
<AlertCircle className="size-4" />
|
||||||
@@ -191,7 +200,10 @@ export function FlowDock({
|
|||||||
<li key={`${issue.code}-${issue.node}-${issue.message_name}`}>
|
<li key={`${issue.code}-${issue.node}-${issue.message_name}`}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="w-full rounded-sm px-2 py-1.5 text-left text-sm transition-colors hover:bg-accent/50 disabled:cursor-default disabled:hover:bg-transparent"
|
className={cn(
|
||||||
|
"w-full rounded-sm px-2 py-1.5 text-left text-sm transition-colors hover:bg-accent/50 disabled:cursor-default disabled:hover:bg-transparent",
|
||||||
|
issue.advisory && "text-muted-foreground",
|
||||||
|
)}
|
||||||
onClick={() => issue.node && onFocusNode(issue.node)}
|
onClick={() => issue.node && onFocusNode(issue.node)}
|
||||||
disabled={!issue.node}
|
disabled={!issue.node}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -336,7 +336,10 @@ function FlowEditorInner({
|
|||||||
const issuesByNode = useMemo(() => {
|
const issuesByNode = useMemo(() => {
|
||||||
const map = new Map<string, string[]>()
|
const map = new Map<string, string[]>()
|
||||||
for (const issue of issues) {
|
for (const issue of issues) {
|
||||||
if (!issue.node) continue
|
// A node's dot and border are its status channel, and status has no
|
||||||
|
// warning tier — an advisory is not a fault, so it says its piece in the
|
||||||
|
// dock rather than painting the node red.
|
||||||
|
if (!issue.node || issue.advisory) continue
|
||||||
const list = map.get(issue.node) ?? []
|
const list = map.get(issue.node) ?? []
|
||||||
list.push(issue.message)
|
list.push(issue.message)
|
||||||
map.set(issue.node, list)
|
map.set(issue.node, list)
|
||||||
|
|||||||
Reference in New Issue
Block a user