Skip to content

[TEMP - do not merge] review slice: model the include on every entry core can splice - #9668

Closed
Mohamed Hessien (m7md7sien) wants to merge 4 commits into
Azure:mainfrom
m7md7sien:temp/review-ref-on-every-entry
Closed

[TEMP - do not merge] review slice: model the include on every entry core can splice#9668
Mohamed Hessien (m7md7sien) wants to merge 4 commits into
Azure:mainfrom
m7md7sien:temp/review-ref-on-every-entry

Conversation

@m7md7sien

Copy link
Copy Markdown

Review-only slice. Will be closed without merging. Please do not spend maintainer time here.

The real change already merged into feat/azure-ai-evaluations-consolidated as #7 on the
fork, which feeds #9500. It is here only because Copilot's re-review of that PR failed
with "Copilot encountered an error and was unable to review this pull request", so the
commit that answered its first review was merged without ever being reviewed.

This carries the 11 touched files at that merged state, so the diff is small enough to
review. It will not build on its own -- the rest of the extension is not here.

What to look at

  • internal/cmd/catalog.go -- refuses a catalog name whose entry lives in another file,
    both when the entry is a pure \ (no name visible) and when it carries an overlay
    name. The second case is the one the earlier review caught: updating it in place
    writes source: beside the directive, and resolution then yields both a spliced rubric
    and a source, which the next read rejects as declaring the rubric twice.
  • internal/project/eval_config.go and schemas/azure.ai.eval.json -- \ is now
    modelled on dataset and eval entries, not just evaluators, so a config that deploys can
    also be opened by the commands that edit it.
  • internal/project/service_target_eval.go -- an include reached without a project root
    is refused rather than discarded.
  • internal/messages/messages.go -- a relative source:/file: inside a \'d
    entry resolves against the configuration, not that file. Not fixed by design; the
    diagnostics now say so.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
7 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Review-only slice aligning $ref handling across evaluation configuration entries and catalog edits.

Changes:

  • Models $ref on datasets, evaluators, and evals.
  • Prevents catalog commands from modifying included entries.
  • Adds strict resolution diagnostics and regression tests.

Reviewed changes

Copilot reviewed 10 out of 11 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
README.md Documents evaluation configuration and includes.
schemas/azure.ai.eval.json Defines $ref-capable entry schemas.
internal/cmd/catalog.go Guards included catalog entries.
internal/cmd/catalog_include_test.go Tests evaluator include guards.
internal/messages/messages.go Adds diagnostics and CLI messages.
internal/project/config_keys_test.go Pins configuration keys.
internal/project/eval_config.go Models $ref fields.
internal/project/ref_on_every_entry_test.go Tests editable entry includes.
internal/project/ref_resolution_test.go Tests include resolution behavior.
internal/project/service_config_strict_test.go Tests strict service decoding.
internal/project/service_target_eval.go Resolves deploy configuration and paths.
Suppressed comments (1)

cli/azd/extensions/azure.ai.evaluations/schemas/azure.ai.eval.json:83

  • This has the same nested-include mismatch as file:: a relative source: inside an entry-level $ref resolves against azure.eval.yaml, not the referenced file. Update the schema text so editor guidance matches the README and runtime diagnostic. [azd-code-reviewer]
          "description": "Path to a local .json rubric -- a list of weighted scoring dimensions -- relative to this file. Published on deploy and fingerprinted locally so a later deploy can tell an edit here from a version published elsewhere."

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

},
"file": {
"type": "string",
"description": "Path to a local .jsonl whose rows are uploaded on deploy, relative to this file. Deliberately not a $ref: the rows are a data artifact to publish, not a definition to splice in, and a .jsonl is neither YAML nor JSON. Omit to use a dataset already registered under this name."
Comment on lines +138 to +151
cfg, err := EvalConfigFromService(serviceConfig, p.projectRoot(ctx))
if err != nil {
return nil, err
}
if err := cfg.Validate(); err != nil {
return nil, messages.EvalConfigInvalid(err)
}

reconciler, err := p.newReconciler(ctx)
if err != nil {
return nil, err
}

baseDir := p.evalBaseDir(ctx, serviceConfig)
name: support-agent
```

`azd up` reconciles **datasets → evaluators → eval groups**, in that order,
// Progress markers from the azd style guide, so the extension's lines sit
// alongside core's without a second vocabulary.
const (
doneMark = "(Γ£ô) Done:" // finished successfully
Comment on lines +549 to +550
var local *azdext.LocalError
if errors.As(err, &local) {
for _, value := range values {
quoted = append(quoted, fmt.Sprintf("%q", value))
}
sort.Strings(quoted)
Comment on lines +103 to +105
if kind == "dataset" {
if decl, ok := cfg.DatasetDeclaration(name); ok {
return decl.Ref, true
Copilot AI review requested due to automatic review settings August 20, 2026 22:36
@m7md7sien

Copy link
Copy Markdown
Author

Thanks -- triaged all seven. Five were actionable, two were my own fault. Fixes are pushed to this branch (e1364f2b5) and land in the real PR via m7md7sien/azure-dev#8.

This slice is still not for merging. It exists only because Copilot's re-review of the fork PR failed with "Copilot encountered an error and was unable to review this pull request", so the commit that answered the previous review merged unreviewed. Please don't spend maintainer time here.

# Comment Disposition
1 \ paths described as "relative to this file" (schema :64, :83) Fixed. Correct, and it contradicted the README and both runtime diagnostics. Both now say the path resolves against the evaluation configuration even when the entry arrived through a \, and steer to definition:.
2 Project root fetched twice in Deploy Fixed. Fetched once and reused. The include guard passed on the first call while artifact paths could still fall back to the process directory on the second.
3 Mojibake in README.md Not a defect -- my error. See below.
4 doneMark check mark corrupted Not a defect -- my error. See below.
5 errors.As to errors.AsType[T] Out of scope, pre-existing. Not introduced here and not flagged by the gate -- see below.
6 sort.Strings to slices.Sort Out of scope, pre-existing. Same.
7 Dataset branch of the include guard untested Fixed. Correct and worth catching. Added both include shapes for the dataset kind, plus the editable-in-place case that must not be refused.

On 3 and 4 -- the encoding findings were an artifact of this slice

The files are clean. Verified at byte level: valid UTF-8, no BOM, zero U+FFFD, and doneMark is the expected check mark. Every non-ASCII codepoint is a legitimate em dash, arrow, middot, or that check mark.

The corruption was introduced when I built this slice by piping git show through PowerShell, which decoded the bytes in the console codepage and re-encoded them. The review was correct about what it saw and wrong about the code -- it was reading my mistake.

Fixed at the source: every file in this branch is now copied with git checkout, and all 14 are byte-identical to the originals. Worth noting for future slices, since it produces findings that look real and are not.

On 5 and 6 -- checked rather than dismissed

Both predate this work: errors.As appears 40 times under cli/azd/pkg and sort.Strings 5 times, plus more across other extensions. They surfaced here only because the slice presents whole files rather than a diff, so unchanged code reads as new.

The cited AGENTS.md guidance is real. The enforcement claim is not quite: lint-ext-azure-ai-evaluations.yml runs go fix -diff ./... against this module, and that gate rewrites neither pattern. A sweep would be worthwhile as its own change.

That check did find one real thing the review missed, in code this PR introduces: go fix wants the slice walk in containsRefDirective as slices.ContainsFunc. That would have failed CI on the real PR. Fixed.

Three further bugs, from a parallel review of the same delta

Not in the review above; the highest-severity one is the kind this feature keeps producing.

  • An evaluator already carrying its rubric under definition: was corrupted by the catalog commands. No \ involved: addEvaluatorToCatalog matched on name and wrote source: beside the existing definition:. The next read rejects the entire configuration for declaring the rubric twice -- after the generation job has been billed and the file written. The guard from comment 7 now covers this shape too.
  • The ownership predicate had two copies and one test. source == "" && definition == nil lived in CustomEvaluators and again inline in eval create. Reverting the untested copy to source == "" left the suite green and eval create silently publishing nothing -- the exact bug that already shipped once. Both now call one method, and a structural test fails the build if a third copy appears.
  • init can still append a duplicate eval behind a pure \, because it checks only the unresolved configuration. Tracked, not fixed here.

Each new guard was mutation-tested: bug reintroduced, test confirmed failing, fix restored. A test that cannot fail is worse than none, and this feature's earlier tests all stopped at decoding, which is why the inert-publish bug survived them.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 14 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

cli/azd/extensions/azure.ai.evaluations/internal/project/eval_config.go:89

  • azd-code-reviewer: The resolver accepts $ref on any object (pkg/foundry/includes.go:84-89), but the edit model only adds it to the outer eval. A config with $ref under source, target, or an evaluators[] item deploys after resolution, while OpenEvalConfigForEdit strictly rejects the same file because those nested types do not model $ref. Model these nested include shapes too, or reject them consistently before deploy; otherwise the stated deploy/edit symmetry remains incomplete.
	Source          *SourceDecl            `yaml:"source,omitempty"            json:"source,omitempty"`
	EvaluationLevel string                 `yaml:"evaluation_level,omitempty"  json:"evaluation_level,omitempty"`
	MaxSamples      int                    `yaml:"max_samples,omitempty"       json:"max_samples,omitempty"`
	Evaluators      evalcore.EvaluatorList `yaml:"evaluators,omitempty"        json:"evaluators,omitempty"`
	Target          *Target                `yaml:"target,omitempty"            json:"target,omitempty"`

cli/azd/extensions/azure.ai.evaluations/internal/project/eval_config_store.go:494

  • azd-code-reviewer: Use errors.AsType here instead of the legacy target-variable pattern, as required by the repository's Go 1.26 guidance (cli/azd/AGENTS.md:369-385) and modernization check.
	var errno syscall.Errno
	if !errors.As(err, &errno) {
		return false
	}

Comment on lines +254 to +255
if spliced {
nestSplicedRubrics(resolved)
@m7md7sien
Mohamed Hessien (m7md7sien) requested a balanced review from Copilot August 20, 2026 22:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 14 changed files in this pull request and generated no new comments.

Suppressed comments (2)

cli/azd/extensions/azure.ai.evaluations/internal/project/eval_config_store.go:255

  • [azd-code-reviewer] spliced is document-wide, so any $ref—including the newly supported dataset/eval refs—causes every evaluator with a top-level dimensions key to be rewritten under definition. A hand-written invalid evaluator can therefore be silently accepted and published merely because an unrelated entry is included. Track which evaluator nodes were actually resolved from refs and only nest those nodes.
	if spliced {
		nestSplicedRubrics(resolved)

cli/azd/extensions/azure.ai.evaluations/internal/project/eval_config.go:80

  • [azd-code-reviewer] Modeling $ref only on the outer eval still leaves deployable configurations that editing commands cannot open. ResolveFileRefs recursively resolves any mapping, so a source: or target: containing $ref works on the resolved use/deploy path, while OpenEvalConfigForEdit strict-decodes the raw object and rejects that key because SourceDecl and Target do not model it. Either model nested includes in these shapes and the schema, or reject them consistently before deploy.
	Ref             string                 `yaml:"$ref,omitempty"              json:"$ref,omitempty"`

Copilot AI review requested due to automatic review settings August 20, 2026 23:08
@m7md7sien

Copy link
Copy Markdown
Author

Round two: 1 comment plus 2 suppressed. The encoding findings are gone, which confirms they were the slice artifact and not the code.

The main comment was right, and it is the best catch of either round

spliced was document-wide, so nestSplicedRubrics rewrote every evaluator with a top-level dimensions key as soon as anything in the file used $ref.

Reproduced before fixing. The same evaluator, byte for byte:

# refused -- correct
datasets:
  - name: golden
    file: ./datasets/golden.jsonl
evaluators:
  - name: quality
    dimensions: [{ id: tone, weight: 3 }]
# silently accepted -- the dataset's directive says nothing about this evaluator
datasets:
  - $ref: ./parts/golden.yaml
evaluators:
  - name: quality
    dimensions: [{ id: tone, weight: 3 }]

The first returned missing required parameter "dimensions". The second returned nil and published the mistake to the service as rubric content. So a hand-written error was reported or swallowed depending on an unrelated neighbour, and it bypassed the strict unknown-key check that definition exists to preserve.

That is precisely what the comment above containsRefDirective claims the mechanism rules out. It solved text-scan versus structural and left document-wide versus per-entry, which is the same mistake one level up.

Fixed in 480aa9dcf: the rescue is now gated on whether that entry carried a directive. Mutation-tested -- reverting the gate fails the new test.

One honest limit. When the configuration is itself behind a $ref, its entries do not exist until after resolution and core does not report which node each came from, so per-entry gating is not possible there and the document-wide behaviour remains. That is the layout the README documents. Closing it properly means either resolving the list ourselves, or expressing a referenced rubric as definition: holding the directive, which core splices into the right place and would delete this mechanism entirely. Noted as a follow-up rather than smuggled into this PR.

Suppressed 1 -- correct, but it is a spec change, not a code fix

Verified all three shapes empirically. Each deploys and is then refused by the editing read:

shape resolving read editing read
eval.source accepted line 5: unknown key "$ref"
eval.target accepted line 6: unknown key "$ref"
evaluators[] item accepted line 6: unknown key "$ref"

So the comment is right that the symmetry is incomplete.

I implemented it, and the change is deliberately not included. Modelling the directive on SourceDecl, Target and EvaluatorRef breaks three tests that pin these shapes to the specification, in as many words:

the spec tabulates these five; a sixth is a promise it does not make

the spec's target: is a type and a name

The directive is on exactly three shapes by design, and those tests exist to stop a fourth appearing without the spec saying so. It also needs validation changes -- a reference carrying only a directive currently fails evaluator entry is missing 'evaluator'.

Two ways forward, and it is a product decision rather than a review fix:

  1. extend the spec to allow the include on the nested shapes, then model it; or
  2. refuse it on the resolving path too, so both reads fail instead of disagreeing.

Reverted and raised with the spec owner. The asymmetry is real, and I would rather it be decided than have it land quietly.

Suppressed 2 -- errors.As in eval_config_store.go:494

Same disposition as last round: pre-existing convention, and go fix -diff ./... -- the gate lint-ext-azure-ai-evaluations.yml actually runs -- does not rewrite it. Confirmed clean on this branch. Worth a sweep of its own.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 15 changed files in this pull request and generated no new comments.

Suppressed comments (4)

cli/azd/extensions/azure.ai.evaluations/internal/project/eval_config_store.go:341

  • [azd-code-reviewer] With the normal service-level $ref, splicedEvaluators returns visible=false before azure.eval.yaml is loaded, so this condition treats every resolved evaluator containing dimensions as though that entry had its own $ref. As a result, an inline typo such as dimensions: in azure.eval.yaml is silently nested and deployed by azd up, while LoadEvalConfig rejects the same file. I think we need to preserve entry-level $ref provenance through the outer include instead of rescuing every evaluator when visible is false.
		if visible && !spliced[i] {

cli/azd/extensions/azure.ai.evaluations/internal/project/service_target_eval.go:224

  • [azd-code-reviewer] Project().Get failures are collapsed into an empty root. The new $ref guard catches includes, but an inline config with a relative dataset file: or evaluator source: still reaches baseDirUnder, which resolves it against the extension process directory. That can read the wrong same-named file or report an existing artifact as missing. I think we need to propagate the RPC error, or reject relative artifact paths whenever the project root is unavailable.
	resp, err := p.azdClient.Project().Get(ctx, &azdext.EmptyRequest{})
	if err != nil || resp.GetProject() == nil {
		return ""
	}
	return resp.GetProject().GetPath()

cli/azd/extensions/azure.ai.evaluations/internal/project/eval_config_store.go:217

  • [azd-code-reviewer] This byte scan also matches $ref in comments and ordinary string values. Those files are unnecessarily unmarshaled and re-marshaled, so strict-decoder diagnostics can point at reordered generated lines instead of the lines the user authored. After parsing, return the original bytes unless the structure actually contains a $ref directive.

This issue also appears on line 341 of the same file.

	if !bytes.Contains(data, []byte("$ref")) {

cli/azd/extensions/azure.ai.evaluations/README.md:116

  • [azd-code-reviewer] The example is a JSON object containing a dimensions list, not a JSON list itself. Calling the rubric a list can lead users to omit the required object wrapper.
A custom rubric is a JSON list of weighted dimensions:

@m7md7sien

Copy link
Copy Markdown
Author

Closing: this was a review-only slice, never for merging. Its purpose is served -- both rounds of findings are triaged and the real fixes are merged into the branch behind #9500. Remaining known gaps are written up in cli/azd/extensions/azure.ai.evaluations/docs/KNOWN-GAPS.md on that branch.

@m7md7sien
Mohamed Hessien (m7md7sien) deleted the temp/review-ref-on-every-entry branch August 21, 2026 00:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants