Documentation
¶
Overview ¶
Package detect works out how to build and run an app from its source.
Two rules shape everything here. R-102: ask, never guess — an uncertain detector returns a question, not a plausible default. R-021: fill declared slots, never invent topology — if the repo does not say it needs a database, Pando does not go hunting through imports to decide that it does.
The auction (R-093) exists so the user can see how the answer was reached. Every detector bids, the bids are ranked, and the runners-up are returned alongside the winner — a verdict with its reasoning, rather than a verdict.
Package detect runs the detector auction and the trial run.
Ask, never guess (R-102). Every builder adapter bids; the winner emits a draft spec with evidence and questions. Questions are held to R-105: answerable by a model that cannot see the repo, because the intended workflow is pasting them into the assistant that wrote the app. The trial run turns unanswerable questions into observations — watch what the app binds rather than asking (R-097). See design 07 sequence A.
Index ¶
- Constants
- func ApplyTrial(draft Draft, questions []Question, t Trial) (Draft, []Question)
- func Assemble(appID string, src spec.Source, draft Draft) spec.AppSpec
- func MayServeWeb(port int, answeredHTTP bool) bool
- func NeedsBuild(s *spec.AppSpec) bool
- func PathRoutingWarning(src api.SourceView, draft Draft) []spec.Warning
- func QuestionKindChoice() api.QuestionKind
- func Report(ctx context.Context, stage string, partial *Proposal)
- func ServesAPage(status int) bool
- func StatusFor(winner Candidate, questions []Question) string
- func Steers(ctx context.Context) bool
- func ValidateAll(questions []Question) error
- func WithBuild(ctx context.Context, fn BuildFunc) context.Context
- func WithProgress(ctx context.Context, fn ProgressFunc) context.Context
- func WithSteer(ctx context.Context) context.Context
- type Auction
- type BuildFunc
- type BuildOutcome
- type BuildPlanner
- type Builder
- type BuildpackDetector
- type Built
- type Candidate
- type ComposeDetector
- type Detector
- type DockerfileDetector
- type Draft
- type ImageReader
- type Job
- type MonorepoDetector
- type ProgressFunc
- type Proposal
- func (p Proposal) Answers(answers map[string]string) map[string]string
- func (p Proposal) BuildWaitsOn(answers map[string]string) []string
- func (p Proposal) CheckAnswers(answers map[string]string) error
- func (p Proposal) RedetectsOn(answers map[string]string) bool
- func (p Proposal) Subproject(answers map[string]string) (dir, why string, ok bool)
- func (p Proposal) TrialSummary() api.TrialSummary
- func (p Proposal) WithAnswers(answers map[string]string) spec.AppSpec
- type PublishedImage
- type Question
- type RegistryProbe
- type Result
- type StaticDetector
- type Suggestion
- type Trial
- type TrialObservation
- type TrialRunner
- type Turn
Constants ¶
const ( KeyPrimaryPort = "primary_port" KeyPrimaryService = "primary_service" KeyStartCommand = "start_command" KeyDockerfilePath = "dockerfile_path" KeyDeployableProject = "deployable_project" KeyStaticSource = "static_source" KeyBuildStrategy = "build_strategy" KeyBuildMethod = "build_method" // BuildArgKeyPrefix, followed by an ARG's name, asks for a build argument // the Dockerfile requires. The answer is passed to the build. BuildArgKeyPrefix = "build_arg." )
Question keys detection produces. Answers are matched back by these, so they are a small fixed vocabulary rather than free-form strings.
const ( StatusReady = "ready" StatusNeedsAnswers = "needs_answers" StatusUnknown = "unknown" // StatusBlocked is not "Pando does not know". It is "Pando knows, and the // answer is that this cannot run as written" — which is a different thing // to show a user, and the reason travels with it. StatusBlocked = "blocked" )
Statuses a detection can end in.
const ( TurnPerson = "person" TurnAI = "ai" )
Who a Turn is from.
const ( StageFetching = "fetching" // cloning the repository StageDetecting = "detecting" // detectors bidding on what the app is StageBuilding = "building" // building the image the app would deploy (R-311) StageTrying = "trying" // a trial run of the winning draft (R-097) StageScanning = "scanning" // a security scan of the source and the image, when a scanner is configured StageScreening = "screening" // an AI adapter repairing the plan or answering its questions (R-336) )
Detection's stages, in the order they happen, as reported while it runs. The console's onboarding page builds itself from them: it shows what Pando has found so far rather than a spinner and then everything at once.
Two run beside the others rather than in turn: the scan of the source starts as soon as the auction has read the repository, alongside the build, and the scan of the built image runs alongside the trial. A running detection reports its stage — the step the rest wait on — and every step under way (Running), so a client shows both while both run.
const DefaultTrialTimeout = 90 * time.Second
DefaultTrialTimeout is how long an app gets to start and bind.
Generous, because a first run pulls a base image and a JVM or a Rails app can take most of a minute to come up — and an app wrongly judged dead costs a question the whole trial run exists to avoid.
const StrategyUnknown spec.BuildStrategy = "unknown"
StrategyUnknown is what detection reports when it could not arrive at a build strategy. It is not a spec.BuildStrategy anyone can deploy — a spec never carries it — which is why it lives here rather than in the spec package.
Variables ¶
This section is empty.
Functions ¶
func ApplyTrial ¶
ApplyTrial folds a trial run's observations into a draft and its questions.
This is where R-097 pays for itself. A port that was going to be a question becomes an observation, and the question disappears rather than being asked of someone R-005 says may not know what a port is.
func Assemble ¶
Assemble turns a draft into a spec that can be shown, diffed and pinned.
A plain function because answering the tie-break needs it too: adopting the other candidate's reading means assembling that candidate's draft, and doing it a second way is how the two would drift.
A build that reaches above the app's directory moves the directory up to where it reaches (liftBuildContexts): what the build reads is decided here, once, for whichever reading is accepted.
func MayServeWeb ¶ added in v0.5.0
MayServeWeb reports a watched port that could be where an app serves: one outside the ephemeral range that answered HTTP or, failing an answer, carries no protocol known not to be HTTP (see showsWebPort).
func NeedsBuild ¶ added in v0.5.0
NeedsBuild reports whether a spec runs an image Pando builds: not a published image, and for a compose file, at least one service with a build of its own.
func PathRoutingWarning ¶
func PathRoutingWarning(src api.SourceView, draft Draft) []spec.Warning
PathRoutingWarning produces WARN_PATH_ROUTING_INCOMPATIBLE (R-168).
A warning, never a fix. R-028 is the rule it serves: Pando does not rewrite an app's response bodies to make path routing work, because an app that only works because its HTML was rewritten in flight is an app that breaks in ways nobody can debug. So where this is likely to bite, it is said out loud and left to the user.
The signal is an asset reference rooted at "/" in the app's own HTML. Served under a path prefix, "/static/app.js" resolves against the domain rather than the prefix, and the app loads a blank page with no error anywhere.
func QuestionKindChoice ¶
func QuestionKindChoice() api.QuestionKind
QuestionKindChoice is a small indirection so question.go does not import api.
func ServesAPage ¶ added in v0.5.0
ServesAPage reports an HTTP status that means a person would see something: a page, a redirect to one, or a sign-in. 204 and 205 answer with no content, which is what a health or metrics endpoint says, not a page.
func StatusFor ¶
StatusFor recomputes detection status after the trial run, and again after a screening — an amendment can answer the last outstanding question, and a status saying answers are needed while asking for none is one somebody has to open the database to understand.
Low confidence on its own no longer says needs_answers. The threshold exists so that a shaky read is not presented as settled, and while a modest bid always carried a question that was the same thing said twice — but once the buildpack detector stopped asking about a port it already had a default for, a plain Go module came back as "needs_answers" with an empty question list. A status that says answers are needed while asking for none is one somebody has to open the database to understand.
Nothing consumed it as a blocker, which is why this is a correction to what the API says rather than to what it does: the console branches on running, failed and blocked, and both the accept handler and the accept button gate on unanswered questions.
func ValidateAll ¶
ValidateAll checks every question, reporting all failures at once.
Accumulating rather than stopping at the first is deliberate: this runs in tests and in review, and someone fixing detection deserves the whole list.
func WithBuild ¶ added in v0.5.0
WithBuild returns a context whose detection builds through fn after the auction and before the trial run. A context and not a field on Job for the reason WithProgress is: one Job serves every app, and the build needs the app's own answers, defaults and checkout.
func WithProgress ¶
func WithProgress(ctx context.Context, fn ProgressFunc) context.Context
WithProgress returns a context whose detection reports its stages to fn. A context and not a field on Job, because one Job serves every app's detection at once and each has its own listener.
func WithSteer ¶ added in v0.5.0
WithSteer returns a context whose build may be steered: the builder may revise a plan it made from convention when a build of it fails in a way it recognizes, and report the plan it built from (api.BuildRequest.Steer). Detection's first build of a proposal asks for it, while the plan is still Pando's own; a deploy never does, and builds what the spec says (R-020).
Types ¶
type Auction ¶
type Auction struct {
// contains filtered or unexported fields
}
Auction runs every detector and ranks the results.
func NewAuction ¶
func (*Auction) Run ¶
Run bids every detector against the source and ranks them.
A close second is not discarded: when two detectors land within closeEnough of each other the auction adds a question rather than breaking the tie itself. Picking between a compose file and a Dockerfile on a 0.02 confidence difference is exactly the guess R-102 forbids.
type BuildFunc ¶ added in v0.5.0
type BuildFunc func(ctx context.Context, partial *Proposal) (BuildOutcome, error)
BuildFunc builds what a proposal would deploy, once the auction has read the repository. It decides whether there is anything to build yet — a required build argument nobody has given means there is not — and returns an error only for a build that failed, which fails the detection with its log (R-107).
type BuildOutcome ¶ added in v0.5.0
type BuildOutcome struct {
Built *Built
PullAuth *api.RegistryAuth
// Plan is the build plan the image was built from, when the builder
// steered the proposed one after a build of it failed: what the proposal
// carries from then on, so the spec says how its image was made (R-020).
// Nil when the proposed plan was built as it was.
Plan map[string]string
}
BuildOutcome is what a BuildFunc did: the build, when there was one, and the credential a trial pulls its image with.
type BuildPlanner ¶
type BuildPlanner interface {
// The plan's Declared is what in the repository dictated it, or nil when
// convention-matching chose it. The builder knows which it was; detection
// shows it, and cannot derive it without a second copy of the same reading.
Plan(ctx context.Context, src api.SourceView) (api.BuildPlan, error)
// PlanPrebuild is the repository's Dockerfile with the repository's own
// build run before it, when the Dockerfile copies what that build makes
// and the repository does not have it — target/*.war before `mvn
// package`. An empty plan when the Dockerfile builds as it is.
PlanPrebuild(ctx context.Context, src api.SourceView, dockerfile string) (api.BuildPlan, error)
}
BuildPlanner produces the files a buildpack build needs.
Planning reads the repository and decides; it runs nothing from it and builds nothing, so it is not a build and R-024's "never on the host" does not bite. The files it returns are opaque to core; what they mean for how the app runs comes back alongside them, in api.BuildPlan.
type Builder ¶
type Builder interface {
Build(ctx context.Context, req api.BuildRequest) (api.BuildResult, error)
}
Builder produces an image from source, so the trial run has something to run.
type BuildpackDetector ¶
type BuildpackDetector struct {
// Planner turns a repository with no deployment instructions into build
// inputs. Supplied by the builder adapter, because what those inputs are is
// the builder's vocabulary and core does not learn it (R-251): core asks
// "how would you build this", stores whatever comes back, and interprets
// none of it.
//
// Nil means detection still recognizes the language and asks its questions;
// the plan simply is not part of the proposal, and the builder makes one at
// build time instead. That is the behavior every install had before the
// plan was reviewable.
Planner BuildPlanner
}
BuildpackDetector recognizes a project by its language, for repositories that carry no deployment instructions at all.
This is the case R-103 is really about: an app written by someone who never thought about deployment. The bid is deliberately modest, because a language is not a deployment — but modest is not the same as inquisitive, and what it can work out it does not ask. With a planner attached it asks nothing at all.
func (BuildpackDetector) Bid ¶
func (d BuildpackDetector) Bid(ctx context.Context, src api.SourceView) (Candidate, error)
func (BuildpackDetector) Name ¶
func (BuildpackDetector) Name() string
type Built ¶ added in v0.5.0
type Built struct {
// Inputs is spec.BuildInputs of the spec it was built from.
Inputs string `json:"inputs"`
// Source is the checkout it was built from: a commit, or an upload's
// digest (source.Checkout.Identity).
Source string `json:"source"`
// Image is the app's image, by ID or by digest. Empty for a compose app,
// whose services each have their own.
Image string `json:"image,omitempty"`
// Images are a compose app's built services' images, by workload name.
Images map[string]string `json:"images,omitempty"`
}
Built is the image detection built for a proposal (R-311, design 09 §4.1).
Detection builds what the app would deploy, so the plan shows the score of that image and the trial run has something to start (R-097). A deploy whose spec has the same build inputs, from the same source, runs this image and reuses its scan instead of building again: the score in the plan is the score of what runs.
func (*Built) Matches ¶ added in v0.5.0
Matches reports whether this build is what s would build from source.
type Candidate ¶
type Candidate struct {
Detector string `json:"detector"`
Strategy spec.BuildStrategy `json:"strategy"`
// Confidence is 0..1. It is shown to the user, so it has to mean something
// consistent across detectors: how sure this detector is that its strategy
// is the right one, not how much it would like to win.
Confidence float64 `json:"confidence"`
// Evidence is human-readable and shown in the review UI, so a person can
// see why Pando thinks what it thinks (R-102).
Evidence []string `json:"evidence,omitempty"`
// Questions are what this detector could not determine. Held to R-105.
Questions []Question `json:"questions,omitempty"`
// Draft is what this detector would put in the spec.
//
// Serialized, and it has to be. The auction asks which of two close
// readings is right (see tieBreak), and answering that means adopting the
// other candidate's draft — impossible if the losing drafts were discarded
// when the proposal was stored. They were, so the answer could do nothing
// but stamp the chosen strategy's *name* onto the winner's draft: choosing
// "compose" produced the Dockerfile detector's spec labeled
// `strategy: compose`, which no builder implements, and the app was refused
// at plan time with a message about the builder.
Draft Draft `json:"draft,omitempty"`
// Spec is this candidate's draft, assembled and completed with the
// install's defaults — the thing accepting it would actually pin.
//
// Every candidate carries one, not just the winner, because any of them can
// be adopted by answering the tie-break. Completing only the winner meant an
// adopted runner-up arrived with no builder, no routing and no port, and
// the deploy failed on "0 is not a usable port number" with nothing
// pointing at why.
Spec *spec.AppSpec `json:"spec,omitempty"`
// Blocked is set when a detector recognizes the repository and cannot
// proceed with it — a compose file using a construct that cannot cross the
// boundary (R-099) is the case this exists for.
//
// It is deliberately not an error returned from Bid. The auction drops a
// detector that errors, because one broken detector must not fail
// detection; a blocked candidate dropped the same way would leave the user
// with a buildpack guess and no idea why their compose file was ignored.
// The reason is the most useful thing Pando has here, so the candidate
// stays in the auction and carries it.
Blocked error `json:"-"`
}
Candidate is one detector's proposal.
type ComposeDetector ¶
type ComposeDetector struct{}
ComposeDetector recognizes a repository that says how its parts fit together.
func (ComposeDetector) Bid ¶
func (d ComposeDetector) Bid(_ context.Context, src api.SourceView) (Candidate, error)
func (ComposeDetector) Name ¶
func (ComposeDetector) Name() string
type Detector ¶
type Detector interface {
// Name identifies the detector in the auction result.
Name() string
// Bid returns this detector's reading of the source. A detector that sees
// nothing it recognizes returns zero confidence rather than a guess.
Bid(ctx context.Context, src api.SourceView) (Candidate, error)
}
Detector examines source and bids on how to build it.
type DockerfileDetector ¶
type DockerfileDetector struct {
// Planner, when there is one, runs the repository's own build ahead of a
// Dockerfile that copies its output (BuildPlanner.PlanPrebuild). Nil
// builds the Dockerfile as it is.
Planner BuildPlanner
}
DockerfileDetector recognizes a repository that says how to build itself.
func (DockerfileDetector) Bid ¶
func (d DockerfileDetector) Bid(ctx context.Context, src api.SourceView) (Candidate, error)
func (DockerfileDetector) Name ¶
func (DockerfileDetector) Name() string
type Draft ¶
type Draft struct {
Workloads []spec.Workload `json:"workloads,omitempty"`
Volumes []spec.Volume `json:"volumes,omitempty"`
Slots []spec.Slot `json:"slots,omitempty"`
Build spec.Build `json:"build"`
Health spec.Health `json:"health,omitempty"`
Warnings []spec.Warning `json:"warnings,omitempty"`
}
Draft is the part of a spec a detector can fill in. Tagged because this crosses the API now. It used to be `json:"-"` on the candidate and never left the process, so Go's field names were nobody's problem; serializing it without tags put `Build` and `Workloads` into a payload where every other object is snake_case, and into the console's generated types alongside them.
func ImportCompose ¶
func ImportCompose(src api.SourceView, name string) (Draft, error)
ImportCompose turns a compose file into a draft spec.
R-096: a compose file is a complete answer, not a hint. The author already said what runs, in what order, with which volumes and healthchecks — so this imports it rather than treating it as evidence for a guess.
R-099 is the other half. Some compose constructs cannot survive the boundary Pando puts around an app, and this returns one of two outcomes for each:
rejected — PLAN_COMPOSE_CONSTRUCT_REJECTED, naming the service and the
construct. There is nothing to rewrite it to.
rewritten — imported with different mechanism and the same behavior, and
a WARN_COMPOSE_CONSTRUCT_REWRITTEN saying exactly what changed.
The split is not about severity. It is about whether the app still does what its author meant afterwards. Publishing a host port is a mechanism Pando replaces with its own routing and the app is still reachable, so it is rewritten. `privileged: true` has no replacement that keeps the isolation the rest of the system depends on, so it is refused.
func WithExampleCompose ¶ added in v0.5.0
WithExampleCompose gives a draft built from the repository itself the backing services an example compose file elsewhere in the repository runs beside it.
Yopass builds from its own Dockerfile and keeps its compose files under deploy/docker-compose/, where each runs the project's published image beside a memcached and points MEMCACHED at it. The Dockerfile won the auction, the compose files were never read, and the app deployed without the cache: pages served, and the first secret anybody stored failed on "dial tcp 127.0.0.1:11211: connect: connection refused". The trial run cannot see it, because yopass connects on its first write, not at start.
That compose file is the author declaring the dependency (R-021), and R-130 names a compose image name as one of the ways a slot is typed, so this is a slot Pando fills and not topology it invents. Only what the example wires to a service Pando provisions is taken — the variable that reaches it and the slot it fills, provisioned as the example runs it [P] — and only from a service that is this app: one that builds from the app's own directory, or that runs the image named for the repository it came from. The rest of the example (its ports, its proxy, its other settings) describes one way to run the app and is not imported; R-096's "complete answer" is a compose file at the root, which is a different reading and bids for itself.
type ImageReader ¶
type ImageReader interface {
Inspect(ctx context.Context, appID, reference string, want oci.Platform) (oci.Inspection, error)
Auth(ctx context.Context, appID, reference string) (*oci.Auth, error)
}
ImageReader reads an app's image from its registry, with the app's own registry credential (oci.Images).
type Job ¶
type Job struct {
Auction *Auction
Registry RegistryProbe
Runtime TrialRunner
Builder Builder
// Images reads an image app's image from its registry before the trial,
// with the app's credential: its digest, platforms and configuration.
Images ImageReader
// TrialTimeout bounds the observation. Zero uses DefaultTrialTimeout.
TrialTimeout time.Duration
// NewTrialID names the throwaway bundle. Injectable so a test can assert
// what was created and cleaned up.
NewTrialID func() string
}
Job runs detection: Sequence A steps 6 through 12.
Every collaborator is optional, and each one missing degrades rather than fails. That is R-106's shape applied to the whole job: with nothing configured, each step turns back into a question, not a dead end.
func (*Job) Run ¶
func (j *Job) Run(ctx context.Context, appID string, src spec.Source, view api.SourceView) (Proposal, error)
Run detects how to build and run the source at src.
Order matters and follows Sequence A. The registry check comes before the auction because a published image makes the auction moot; the trial run comes after, because there is nothing to run until a draft exists.
type MonorepoDetector ¶
type MonorepoDetector struct{}
MonorepoDetector recognizes a repository that holds many projects rather than one app.
This bids StrategyUnknown, which is not a contradiction: being confident that a repository is not a single deployable app is a real detection result, and a more useful one than a low-confidence guess at which of its twenty packages was meant. R-021 is the rule — Pando fills what is declared and does not invent topology — and a workspace manifest is the repository declaring, in as many words, that it contains several things.
It is a veto rather than a bid, so it stands down whenever the repository does say what to deploy: a Dockerfile or compose file at the root answers the question this detector would otherwise ask.
func (MonorepoDetector) Bid ¶
func (d MonorepoDetector) Bid(_ context.Context, src api.SourceView) (Candidate, error)
func (MonorepoDetector) Name ¶
func (MonorepoDetector) Name() string
type ProgressFunc ¶
ProgressFunc hears each stage as detection reaches it, with the proposal as far as it has got — nil before there is one.
type Proposal ¶
type Proposal struct {
Status string `json:"status"`
Winner Candidate `json:"winning_bid"`
RunnersUp []Candidate `json:"runners_up"`
Questions []Question `json:"questions"`
DraftSpec spec.AppSpec `json:"draft_spec"`
// Blocked is the reason, when the best reading of this repository is one
// that cannot proceed — a compose file using a construct that cannot cross
// the boundary (R-099).
Blocked *errs.Error `json:"blocked,omitempty"`
// TrialLog is the app's own output from the trial run. On a crash this is
// the entire answer Pando has, and R-107 says showing it and stopping is
// the correct outcome rather than a gap to close with inference.
TrialLog string `json:"trial_log,omitempty"`
// Commit is what was actually read. R-120: Ref is what the user asked for,
// Commit is what runs.
Commit string `json:"commit,omitempty"`
// Trial is what the trial run observed, minus the log, which TrialLog
// already carries.
//
// Kept on the proposal because the log is not the whole of what the trial
// established and two things need the rest of it. A screening pass has to
// know whether the app crashed, because that is what decides whether a
// dependency it found may be marked required (O-4's fallback, design 10
// §3.2). And "no trial ran" and "a trial ran and saw nothing" are different
// facts that an empty log reports identically.
Trial TrialObservation `json:"trial,omitzero"`
// LoopbackCommand is the command the trial started the app with when it
// listened only on 127.0.0.1, where it can be told: the answer to the
// question that asks for a command that moves it cannot be this one
// (CheckAnswers).
LoopbackCommand string `json:"loopback_command,omitempty"`
// Built is the image detection built for this proposal, when it built one
// (R-311): what the plan's score was read from, and what a deploy of the
// same build inputs runs instead of building again. Nil for an app that
// runs a published image, and for one whose build waits on an answer.
Built *Built `json:"built,omitempty"`
// Screening is what an AI adapter made of this proposal (R-331, design 10
// §5): which adapter, which model, what it read, what it changed and why,
// and what it asked for that Pando refused.
//
// On the proposal rather than only in the audit log, because the console
// renders it as a section of the review. R-334 calls for attribution, and
// attribution nobody can see is a record rather than attribution.
//
// Nil means no screening ran, which is the ordinary case for an install
// with no AI adapter configured — and not a degraded one (R-335).
Screening *screening.Outcome `json:"screening,omitempty"`
// Conversation is what a person reviewing this proposal asked an AI adapter
// to change, and what it replied and did (R-336's third trigger), oldest
// first. On the proposal because it is about this reading of this commit:
// detecting again starts a new one.
Conversation []Turn `json:"conversation,omitempty"`
}
Proposal is what a detection job produces (design 04 §2.2).
A proposal, never a deployment (R-093). It is reviewed, and only then pinned as spec revision 1 — accepting is not deploying, which is the assertion Sequence A makes twice.
func (Proposal) Answers ¶
Answers is every answer in effect: an AI adapter's suggestions, overridden by whatever a person answered (R-338).
func (Proposal) BuildWaitsOn ¶ added in v0.5.0
BuildWaitsOn is the open questions a build of this proposal waits on: those whose answers change what would be built. Empty when it can be built now.
func (Proposal) CheckAnswers ¶
CheckAnswers refuses an answer that cannot become a spec, when it is given.
A build_method or build_strategy answer selects a reading of the repository, and only a reading some detector made has anything to run. Any other answer was recorded, applied to nothing, and refused at accept with "This app has no workloads" — after the person had moved on (issue #55). The AI adapter's prose answers ("serve the repository root with php -S …") were accepted the same way.
func (Proposal) RedetectsOn ¶ added in v0.5.0
RedetectsOn reports whether these answers change which directory this proposal should have read, so detection runs again rather than the image being rebuilt (Subproject).
func (Proposal) Subproject ¶ added in v0.5.0
Subproject is the directory, within the one this proposal read, that detection reads instead: the project a person named in answer to deployable_project, or the only project a repository with no app at its root holds. why says which, for the evidence. ok is false when the proposal describes what to deploy as it stands.
Answering deployable_project used to set the subdirectory and nothing else: the proposal still described the repository root, which had no workloads, and accepting it pinned a spec that ran nothing (issue #90). The project's own reading is what there is to accept, so detection runs again in it.
func (Proposal) TrialSummary ¶
func (p Proposal) TrialSummary() api.TrialSummary
TrialSummary is the trial as an AI screener sees it (R-330, design 10 §2).
func (Proposal) WithAnswers ¶
WithAnswers folds a person's answers, and any AI adapter's suggestions they did not override, into the draft spec.
An answer is worth more than anything Pando worked out for itself, so it overwrites rather than merges — and a port supplied by a person is recorded with Source "user", which is what the review UI shows beside it (design 01 §2.3). Where the value came from is part of the value, so a port that came from a suggestion is recorded as screened instead: the review shows which it was, and spec.Carry does not keep it across a re-detection as though somebody had chosen it.
Suggestions go through the same machinery as a person's answers, deliberately: an answer that changes which workload the other answers mean does so whoever supplied it, and two ways to apply an answer is how the two would drift.
type PublishedImage ¶
type PublishedImage struct {
Ref string `json:"ref"`
Digest string `json:"digest,omitempty"`
Registry string `json:"registry"`
}
PublishedImage is an image that already exists.
type Question ¶
type Question struct {
Key string `json:"key"`
Prompt string `json:"prompt"`
Why string `json:"why"`
Kind api.QuestionKind `json:"kind"`
Options []string `json:"options,omitempty"`
// Deferred marks a question the trial run is expected to answer (R-097).
//
// This is the difference between something Pando does not know and something
// a person has to tell it. A port is the clearest case: R-097 exists so that
// Pando watches the app bind rather than asking, and R-005 says the person
// deploying may not know what a port is. Holding such a question against the
// questions-per-deploy budget would count a cost nobody pays.
//
// The question is still carried, because the trial run can fail to observe
// an answer — at which point it stops being deferred and is put to a person.
Deferred bool `json:"deferred,omitempty"`
// Suggested is an AI adapter's answer to this question (R-338).
//
// Carried on the question rather than folded into the draft spec, so the
// question stays on the page and a person can see the answer, check its
// reason, and change it with the same request that answers any question.
// It counts as an answer until a person gives one; a person's answer wins.
Suggested *Suggestion `json:"suggested,omitempty"`
}
Question is something detection could not work out.
Prompt carries a hard content requirement from R-105: it must be answerable by a model that cannot see the repository, because the expected workflow is pasting it into the assistant that wrote the app. See Validate.
func Asked ¶
Asked returns the questions a person has to answer.
Deferred questions are excluded: they are Pando's to resolve by watching the app start, not the user's to answer before it does.
func Open ¶
Open returns the asked questions nobody has answered: no answer from a person in answers, and no suggestion from an AI adapter.
func (Question) Validate ¶
Validate checks a question against R-105.
R-105 is a content requirement, not a style note. The expected workflow is a person pasting the question into the assistant that wrote their app — so the question has to be answerable by a model that cannot see the repository, and cannot see Pando either.
The design gives the bar directly:
"Which port?" fails. "This app appears to be a Node.js service. Pando could not determine which port it serves HTTP on. Valid answer: a port number such as 3000." passes.
The checks below are necessary conditions, not sufficient ones — no linter can confirm a question is genuinely answerable. They catch the failures that actually happen: a bare fragment, a question that assumes the reader can see the repo, and one that never says what a good answer looks like.
type RegistryProbe ¶
type RegistryProbe interface {
// Published reports images published for a source repository, best first.
// No result is the normal case and not an error.
Published(ctx context.Context, src spec.Source) ([]PublishedImage, error)
}
RegistryProbe looks for an image the maintainer already publishes.
R-094 tier 1, the top of the confidence ladder, and it sits there because a published image is the maintainer's own answer to "how is this built" — already built, already shipped by whoever maintains it. Nothing Pando infers about the source beats it.
It is part of detection, not an adapter (issue #153): the planner asks it nothing, and the OCI distribution API it speaks is no provider's vocabulary (design 03 §8.1). The registry Pando pushes built images to is the adapter.
type Result ¶
type Result struct {
Winner Candidate `json:"winning_bid"`
RunnersUp []Candidate `json:"runners_up"`
// Questions are the winner's, plus anything the auction itself could not
// settle — such as which of two close bids is right.
Questions []Question `json:"questions"`
Status string `json:"status"`
// Blocked is the winner's reason, when the best reading of this repository
// is one that cannot proceed. Surfacing it is the whole point: "your
// compose file sets privileged: true, and here is why that cannot run" is
// an answer, and silently falling through to a language guess is not.
Blocked error `json:"-"`
}
Result is the outcome of an auction.
type StaticDetector ¶
type StaticDetector struct{}
StaticDetector recognizes a site with no server.
func (StaticDetector) Bid ¶
func (d StaticDetector) Bid(_ context.Context, src api.SourceView) (Candidate, error)
func (StaticDetector) Name ¶
func (StaticDetector) Name() string
type Suggestion ¶
type Suggestion struct {
Value string `json:"value"`
Reason string `json:"reason,omitempty"`
Evidence []string `json:"evidence,omitempty"`
}
Suggestion is an answer an AI adapter gave, with why and from what.
type Trial ¶
type Trial struct {
// Ran is false when no trial happened — no runtime configured, or one that
// does not support it. Distinguished from a trial that ran and observed
// nothing, because the first leaves questions unanswered and the second is
// information.
Ran bool
Started bool
Crashed bool
// Exited is a process that finished on its own with status 0 while it was
// watched: a command-line program or a script, not a server (R-107).
Exited bool
ObservedPorts []int
LoopbackPorts []int
// HTTPProbed is true when the runtime sent each observed port an HTTP
// request, and HTTPAnswers are the ports that answered (observedPorts).
HTTPProbed bool
HTTPAnswers []api.HTTPAnswer
// HTTPProbedPorts are the observed ports the runtime actually asked
// (api.TrialResult.HTTPProbedPorts); noHTTPRefusal reads them.
HTTPProbedPorts []int
ObservedWrites []string
ImageVolumes []string
Log string
}
Trial is what a trial run told detection (R-097).
func FromTrialResult ¶
func FromTrialResult(caps api.RuntimeCapabilities, r api.TrialResult) Trial
FromTrialResult reads an adapter's result, given what the runtime said it could observe.
Capabilities are consulted rather than inferred from empty fields: a runtime that cannot observe ports and one that observed no listening port both return an empty slice, and they mean opposite things. The first leaves the question for a person; the second is evidence the app does not serve HTTP.
func (Trial) Observation ¶
func (t Trial) Observation() TrialObservation
Observation is what survives onto the proposal.
type TrialObservation ¶
type TrialObservation struct {
Ran bool `json:"ran,omitempty"`
Started bool `json:"started,omitempty"`
Crashed bool `json:"crashed,omitempty"`
ObservedPorts []int `json:"observed_ports,omitempty"`
ObservedWrites []string `json:"observed_writes,omitempty"`
}
TrialObservation is Trial without the log.
type TrialRunner ¶
type TrialRunner interface {
Capabilities(ctx context.Context) (api.RuntimeCapabilities, error)
Trial(ctx context.Context, req api.TrialRequest) (api.TrialResult, error)
}
TrialRunner starts a draft in throwaway isolation (R-097).
type Turn ¶
type Turn struct {
// From is TurnPerson or TurnAI.
From string `json:"from"`
Text string `json:"text"`
At time.Time `json:"at"`
// Changes and Refused are what an AI turn did to the plan, one line each:
// the applied amendments' summaries, and what Pando would not do with its
// reason (R-334).
Changes []string `json:"changes,omitempty"`
Refused []string `json:"refused,omitempty"`
// FilesRead is what an AI turn read (R-337), so the next one can start
// from it rather than read it all again (api.ScreenRequest.Known).
FilesRead []string `json:"files_read,omitempty"`
}
Turn is one message about a proposal.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package registryprobe looks for images a project already publishes.
|
Package registryprobe looks for images a project already publishes. |