mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-08-06 09:26:52 +01:00
Authoring-assist: promote to first-class tool with non-blocking PR advisory
Promotes the authoring-assist prototype to a first-class BCQuality feature that
reviews knowledge-article front-matter and proposes missing routing `signals:`
(raise triggers and `effect: suppress` suppressors). The suggestion is ADVISORY
ONLY: it never fails a build; authors apply it via a normal PR under existing
R29 + CI + CODEOWNERS review.
Feature:
- tools/Suggest-ArticleSignals.ps1: suggestion engine hardened with a stable JSON
contract (schemaVersion/toolVersion) and a deterministic, effect-sensitive
per-proposal suggestionId, plus -ChangedFiles scoping for PR-diff runs. Runs
self-contained: the routing seed is now OPTIONAL (built-in domain map), so this
does not depend on the separate routing-index tuning thread.
- tools/New-AuthoringAssistComment.ps1: renders one upsertable, feedback-instrumented
advisory comment (stable anchor + aa:meta/aa:article markers carrying suggestionIds
+ reaction footer) for the downstream acceptance-measurement work.
- .github/workflows/authoring-assist*.yml: unprivileged pull_request intake +
trusted workflow_run runner (review/publish split) + in-repo self-test.
- tools/Test-SuggestArticleSignals.ps1: 23-check smoke test (contract, determinism,
scoping, suppressor/prohibition, renderer markers, seed-free graceful degradation).
Dormant schema support:
- .github/scripts/validate_frontmatter.py: adds R29, validating the optional
`signals` block shape (bare token or mapping with token + optional
pattern/domain/effect in {raise,suppress}). Reserved & dormant — no production
pipeline consumes it; the existing PR Reviewer only reads `effect: suppress`
behind its BCQ_INDEX_V2 flag and falls back safely when absent. Existing rules
(incl. R24 skill-id uniqueness, R27, R28) are unchanged.
Excludes the routing-index tuning thread entirely (separate work).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 75d852d6-18a6-4c58-986f-ed5ab16618fa
This commit is contained in:
parent
ad8ccde595
commit
94d3d4358d
8 changed files with 1313 additions and 1 deletions
36
.github/scripts/validate_frontmatter.py
vendored
36
.github/scripts/validate_frontmatter.py
vendored
|
|
@ -34,6 +34,11 @@ KNOWLEDGE_REQUIRED_KEYS = {
|
|||
"bc-version", "domain", "keywords", "technologies",
|
||||
"countries", "application-area",
|
||||
}
|
||||
# Optional knowledge keys (do not trigger the R02 closed-key-set error).
|
||||
# `signals`: OPTIONAL routing-signal declarations. RESERVED & DORMANT — the shape
|
||||
# is validated (R29 below) so authors and the authoring-assist advisory can
|
||||
# populate it, but no production pipeline consumes it yet. See tools/authoring-assist.md.
|
||||
KNOWLEDGE_OPTIONAL_KEYS = {"signals"}
|
||||
ACTION_SKILL_REQUIRED_KEYS = {
|
||||
"kind", "id", "version", "title", "description", "inputs", "outputs",
|
||||
}
|
||||
|
|
@ -203,7 +208,7 @@ def validate_knowledge(path: Path, parsed: Parsed, report: Report) -> None:
|
|||
|
||||
# R02 required keys, no extras, none empty
|
||||
missing = KNOWLEDGE_REQUIRED_KEYS - fm.keys()
|
||||
extras = fm.keys() - KNOWLEDGE_REQUIRED_KEYS
|
||||
extras = fm.keys() - KNOWLEDGE_REQUIRED_KEYS - KNOWLEDGE_OPTIONAL_KEYS
|
||||
if missing:
|
||||
report.error(path, "R02", f"missing required frontmatter keys: {sorted(missing)}", 1)
|
||||
if extras:
|
||||
|
|
@ -213,6 +218,35 @@ def validate_knowledge(path: Path, parsed: Parsed, report: Report) -> None:
|
|||
if v is None or v == "" or v == []:
|
||||
report.error(path, "R02", f"frontmatter key '{k}' must not be empty", 1)
|
||||
|
||||
# R29 optional routing `signals` block. Each entry is either a bare token
|
||||
# string or a mapping with a required 'token' and optional 'pattern'/'domain'/
|
||||
# 'effect' (all non-empty strings; effect in {raise, suppress}). Reserved &
|
||||
# dormant: validated for shape only so the block stays well-formed for the
|
||||
# authoring-assist advisory; no production pipeline consumes it yet.
|
||||
if "signals" in fm:
|
||||
sigs = fm["signals"]
|
||||
if not isinstance(sigs, list) or not sigs:
|
||||
report.error(path, "R29", "signals must be a non-empty list", 1)
|
||||
else:
|
||||
for entry in sigs:
|
||||
if isinstance(entry, str):
|
||||
if not entry.strip():
|
||||
report.error(path, "R29", "signals token string must not be empty", 1)
|
||||
elif isinstance(entry, dict):
|
||||
tok = entry.get("token")
|
||||
if not isinstance(tok, str) or not tok.strip():
|
||||
report.error(path, "R29", "signals entry must have a non-empty 'token'", 1)
|
||||
for opt in ("pattern", "domain"):
|
||||
if opt in entry and (not isinstance(entry[opt], str) or not entry[opt].strip()):
|
||||
report.error(path, "R29", f"signals '{opt}' must be a non-empty string", 1)
|
||||
if "effect" in entry and entry["effect"] not in ("raise", "suppress"):
|
||||
report.error(path, "R29", "signals 'effect' must be 'raise' or 'suppress'", 1)
|
||||
unknown = set(entry.keys()) - {"token", "pattern", "domain", "effect"}
|
||||
if unknown:
|
||||
report.error(path, "R29", f"signals entry has unknown keys: {sorted(unknown)}", 1)
|
||||
else:
|
||||
report.error(path, "R29", "signals entry must be a string or a mapping", 1)
|
||||
|
||||
# R03 bc-version
|
||||
if "bc-version" in fm:
|
||||
_, err = expand_bc_version(fm["bc-version"])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue