@design-ai/cli 4.56.0 → 4.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +109 -25
- package/CHANGELOG.md +36 -0
- package/README.ko.md +9 -26
- package/README.md +10 -23
- package/cli/bin/design-ai.mjs +1 -0
- package/cli/commands/help.mjs +7 -4
- package/cli/commands/index.mjs +292 -0
- package/cli/commands/learn-help.mjs +149 -0
- package/cli/commands/learn-print-profile.mjs +373 -0
- package/cli/commands/learn-print-restore.mjs +349 -0
- package/cli/commands/learn-print-signals.mjs +453 -0
- package/cli/commands/learn.mjs +60 -1281
- package/cli/commands/pack.mjs +7 -3
- package/cli/commands/prompt.mjs +7 -3
- package/cli/commands/search.mjs +94 -1
- package/cli/lib/dispatch.mjs +3 -0
- package/cli/lib/embedding-index.mjs +199 -0
- package/cli/lib/embedding-provider.mjs +121 -0
- package/cli/lib/embedding-rerank.mjs +52 -0
- package/cli/lib/learn-args.mjs +490 -0
- package/cli/lib/learn-backup.mjs +748 -0
- package/cli/lib/learn-curation.mjs +612 -0
- package/cli/lib/learn-eval.mjs +459 -0
- package/cli/lib/learn-profile.mjs +763 -0
- package/cli/lib/learn-select.mjs +223 -0
- package/cli/lib/learn-shared.mjs +158 -0
- package/cli/lib/learn-test-support.mjs +218 -0
- package/cli/lib/learn-usage.mjs +360 -0
- package/cli/lib/learn.mjs +83 -3694
- package/cli/lib/lexical.mjs +137 -0
- package/cli/lib/local-config.mjs +110 -0
- package/cli/lib/mcp-server.mjs +13 -3
- package/cli/lib/pack.mjs +31 -4
- package/cli/lib/prompt.mjs +42 -4
- package/cli/lib/recall.mjs +193 -0
- package/cli/lib/retrieval-index.mjs +0 -0
- package/cli/lib/search-ranked.mjs +170 -0
- package/cli/lib/search.mjs +11 -1
- package/cli/lib/signals-backlog-commands.mjs +673 -0
- package/cli/lib/signals-backlog.mjs +361 -0
- package/cli/lib/signals-eval.mjs +176 -0
- package/cli/lib/signals-readiness.mjs +247 -0
- package/cli/lib/signals-registry.mjs +377 -0
- package/cli/lib/signals-render.mjs +478 -0
- package/cli/lib/signals-shared.mjs +75 -0
- package/cli/lib/signals.mjs +16 -2337
- package/cli/lib/site-bundle-handoff-expected.mjs +173 -0
- package/cli/lib/site-bundle-handoff-runbook-action-summary.mjs +332 -0
- package/cli/lib/site-bundle-handoff-runbook-human-lines.mjs +167 -0
- package/cli/lib/site-bundle-handoff-runbook-maps.mjs +238 -0
- package/cli/lib/site-bundle-handoff-runbook-next-step.mjs +278 -0
- package/cli/lib/site-bundle-handoff-runbook.mjs +44 -931
- package/cli/lib/site-test-support.mjs +68 -0
- package/cli/lib/skill-proposals-apply-commands.mjs +135 -0
- package/cli/lib/skill-proposals-apply-contract.mjs +750 -0
- package/cli/lib/skill-proposals-apply-plan.mjs +118 -0
- package/cli/lib/skill-proposals-generate.mjs +298 -0
- package/cli/lib/skill-proposals-render.mjs +532 -0
- package/cli/lib/skill-proposals-review.mjs +262 -0
- package/cli/lib/skill-proposals.mjs +15 -2046
- package/cli/lib/workspace-args.mjs +83 -0
- package/cli/lib/workspace-git.mjs +169 -0
- package/cli/lib/workspace-learning.mjs +483 -0
- package/cli/lib/workspace-repo.mjs +139 -0
- package/cli/lib/workspace-report.mjs +283 -0
- package/cli/lib/workspace-test-support.mjs +99 -0
- package/cli/lib/workspace.mjs +30 -1056
- package/docs/AI-LEARNING-PHASE2.md +215 -0
- package/docs/DISTRIBUTION.ko.md +2 -2
- package/docs/DISTRIBUTION.md +2 -2
- package/docs/NEXT-SURFACE-DECISION.md +125 -0
- package/docs/PRODUCT-READINESS.md +3 -3
- package/docs/RELEASE-GATES.ko.md +35 -0
- package/docs/RELEASE-GATES.md +234 -0
- package/docs/ROADMAP.md +53 -8
- package/docs/external-status.md +6 -8
- package/docs/inspection-20260630.md +169 -0
- package/docs/integrations/design-ai-mcp-server.md +4 -4
- package/docs/reference/ant-design.md +413 -0
- package/docs/reference/awesome-design-md.md +439 -0
- package/docs/reference/mui.md +783 -0
- package/docs/reference/shadcn-ui.md +298 -0
- package/examples/component-accordion-actions.md +5 -5
- package/examples/component-accordion-details.md +5 -5
- package/examples/component-accordion-summary.md +5 -5
- package/examples/component-accordion.md +3 -3
- package/examples/component-affix.md +1 -1
- package/examples/component-alert-dialog.md +1 -1
- package/examples/component-alert-title.md +1 -1
- package/examples/component-alert.md +3 -3
- package/examples/component-anchor.md +1 -1
- package/examples/component-app-bar.md +1 -1
- package/examples/component-aspect-ratio.md +1 -1
- package/examples/component-auto-complete.md +2 -2
- package/examples/component-avatar-group.md +5 -5
- package/examples/component-avatar.md +3 -3
- package/examples/component-back-top.md +1 -1
- package/examples/component-backdrop.md +1 -1
- package/examples/component-badge.md +3 -3
- package/examples/component-border-beam.md +3 -3
- package/examples/component-bottom-navigation.md +1 -1
- package/examples/component-box.md +1 -1
- package/examples/component-breadcrumb.md +3 -3
- package/examples/component-button-base.md +3 -3
- package/examples/component-button-group.md +2 -2
- package/examples/component-button.md +3 -3
- package/examples/component-calendar.md +2 -2
- package/examples/component-card-actions.md +1 -1
- package/examples/component-card-content.md +1 -1
- package/examples/component-card-header.md +1 -1
- package/examples/component-card-media.md +1 -1
- package/examples/component-card.md +3 -3
- package/examples/component-carousel.md +2 -2
- package/examples/component-cascader.md +1 -1
- package/examples/component-chart.md +1 -1
- package/examples/component-checkbox.md +3 -3
- package/examples/component-click-away-listener.md +1 -1
- package/examples/component-code.md +1 -1
- package/examples/component-collapsible.md +1 -1
- package/examples/component-color-picker.md +1 -1
- package/examples/component-combobox.md +1 -1
- package/examples/component-command.md +1 -1
- package/examples/component-config-provider.md +3 -3
- package/examples/component-context-menu.md +1 -1
- package/examples/component-css-baseline.md +3 -3
- package/examples/component-date-picker.md +2 -2
- package/examples/component-descriptions.md +1 -1
- package/examples/component-dialog-actions.md +1 -1
- package/examples/component-dialog-content-text.md +1 -1
- package/examples/component-dialog-content.md +1 -1
- package/examples/component-dialog-title.md +1 -1
- package/examples/component-dialog.md +2 -2
- package/examples/component-divider.md +3 -3
- package/examples/component-drawer.md +3 -3
- package/examples/component-dropdown.md +3 -3
- package/examples/component-empty.md +2 -2
- package/examples/component-fade.md +1 -1
- package/examples/component-field.md +1 -1
- package/examples/component-filled-input.md +1 -1
- package/examples/component-flex.md +1 -1
- package/examples/component-float-button.md +2 -2
- package/examples/component-form-control-label.md +1 -1
- package/examples/component-form-control.md +1 -1
- package/examples/component-form-controls.md +1 -1
- package/examples/component-form-group.md +1 -1
- package/examples/component-form-helper-text.md +1 -1
- package/examples/component-form-label.md +1 -1
- package/examples/component-form.md +2 -2
- package/examples/component-grid.md +2 -2
- package/examples/component-grow.md +1 -1
- package/examples/component-hover-card.md +1 -1
- package/examples/component-icon-button.md +1 -1
- package/examples/component-icon.md +1 -1
- package/examples/component-image-list.md +1 -1
- package/examples/component-image.md +1 -1
- package/examples/component-input-adornment.md +1 -1
- package/examples/component-input-base.md +1 -1
- package/examples/component-input-number.md +1 -1
- package/examples/component-input-otp.md +2 -2
- package/examples/component-input.md +3 -3
- package/examples/component-item.md +1 -1
- package/examples/component-label.md +1 -1
- package/examples/component-layout.md +1 -1
- package/examples/component-link.md +1 -1
- package/examples/component-list-item-avatar.md +1 -1
- package/examples/component-list-item-button.md +1 -1
- package/examples/component-list-item-icon.md +1 -1
- package/examples/component-list-item-text.md +1 -1
- package/examples/component-list-item.md +1 -1
- package/examples/component-list-subheader.md +1 -1
- package/examples/component-list.md +2 -2
- package/examples/component-masonry.md +1 -1
- package/examples/component-mentions.md +1 -1
- package/examples/component-menu-item.md +1 -1
- package/examples/component-menu-list.md +1 -1
- package/examples/component-menu.md +1 -1
- package/examples/component-menubar.md +1 -1
- package/examples/component-message.md +1 -1
- package/examples/component-mobile-stepper.md +1 -1
- package/examples/component-modal.md +5 -5
- package/examples/component-navigation-menu.md +1 -1
- package/examples/component-notification.md +1 -1
- package/examples/component-outlined-input.md +1 -1
- package/examples/component-pagination.md +3 -3
- package/examples/component-paper.md +1 -1
- package/examples/component-popconfirm.md +1 -1
- package/examples/component-popover.md +3 -3
- package/examples/component-popper.md +1 -1
- package/examples/component-progress.md +2 -2
- package/examples/component-qr-code.md +1 -1
- package/examples/component-radio.md +3 -3
- package/examples/component-rate.md +2 -2
- package/examples/component-resizable.md +1 -1
- package/examples/component-result.md +1 -1
- package/examples/component-scroll-area.md +1 -1
- package/examples/component-segmented.md +3 -3
- package/examples/component-select.md +3 -3
- package/examples/component-separator.md +1 -1
- package/examples/component-sheet.md +1 -1
- package/examples/component-sidebar.md +1 -1
- package/examples/component-skeleton.md +3 -3
- package/examples/component-slide.md +1 -1
- package/examples/component-slider.md +3 -3
- package/examples/component-snackbar-content.md +1 -1
- package/examples/component-snackbar.md +1 -1
- package/examples/component-sonner.md +1 -1
- package/examples/component-space.md +1 -1
- package/examples/component-speed-dial-action.md +1 -1
- package/examples/component-speed-dial.md +1 -1
- package/examples/component-spin.md +2 -2
- package/examples/component-spinner.md +1 -1
- package/examples/component-splitter.md +2 -2
- package/examples/component-stack.md +1 -1
- package/examples/component-statistic.md +1 -1
- package/examples/component-step-button.md +4 -4
- package/examples/component-step-connector.md +4 -4
- package/examples/component-step-content.md +1 -1
- package/examples/component-step-icon.md +1 -1
- package/examples/component-step-label.md +1 -1
- package/examples/component-step.md +2 -2
- package/examples/component-steps.md +2 -2
- package/examples/component-swipeable-drawer.md +1 -1
- package/examples/component-switch.md +3 -3
- package/examples/component-tab-scroll-button.md +5 -5
- package/examples/component-tab.md +1 -1
- package/examples/component-table-body.md +1 -1
- package/examples/component-table-cell.md +1 -1
- package/examples/component-table-container.md +1 -1
- package/examples/component-table-footer.md +1 -1
- package/examples/component-table-head.md +1 -1
- package/examples/component-table-pagination.md +1 -1
- package/examples/component-table-row.md +1 -1
- package/examples/component-table-sort-label.md +1 -1
- package/examples/component-table.md +2 -2
- package/examples/component-tabs.md +3 -3
- package/examples/component-tag-badge.md +2 -2
- package/examples/component-tag.md +1 -1
- package/examples/component-textarea-autosize.md +1 -1
- package/examples/component-textarea.md +1 -1
- package/examples/component-time-picker.md +1 -1
- package/examples/component-timeline.md +1 -1
- package/examples/component-toast.md +4 -4
- package/examples/component-toggle-button.md +1 -1
- package/examples/component-toggle.md +1 -1
- package/examples/component-toolbar.md +1 -1
- package/examples/component-tooltip.md +3 -3
- package/examples/component-tour.md +1 -1
- package/examples/component-transfer.md +1 -1
- package/examples/component-tree-select.md +1 -1
- package/examples/component-tree.md +1 -1
- package/examples/component-typography.md +2 -2
- package/examples/component-upload.md +1 -1
- package/examples/component-watermark.md +1 -1
- package/examples/component-zoom.md +1 -1
- package/knowledge/COVERAGE.md +4 -3
- package/knowledge/components/INDEX.md +202 -202
- package/knowledge/patterns/brand-references.md +72 -72
- package/package.json +1 -1
- package/tools/audit/local-ci.py +16 -1
- package/tools/audit/package-smoke.py +611 -9
- package/tools/audit/registry-smoke.py +534 -0
- package/tools/audit/release-metadata.py +47 -1
- package/tools/audit/smoke_assertions.py +915 -13
- package/tools/migrations/refs-links-to-reference-pages.py +179 -0
|
@@ -39,6 +39,8 @@ from smoke_assertions import (
|
|
|
39
39
|
EXPECTED_HELP_ALIASES,
|
|
40
40
|
EXPECTED_NUMERIC_VALUE_SMOKES,
|
|
41
41
|
EXPECTED_PACK_MAX_BYTES,
|
|
42
|
+
EXPECTED_RANKED_SEARCH_LIMIT,
|
|
43
|
+
EXPECTED_RANKED_SEARCH_NOT_BUILT_NOTICE,
|
|
42
44
|
EXPECTED_REPOSITORY_URL,
|
|
43
45
|
EXPECTED_ROUTE_BRIEF,
|
|
44
46
|
EXPECTED_ROUTE_ID,
|
|
@@ -61,6 +63,9 @@ from smoke_assertions import (
|
|
|
61
63
|
assert_force_overwrite_replaced,
|
|
62
64
|
assert_functional_alias_smokes,
|
|
63
65
|
assert_help_topic_output,
|
|
66
|
+
assert_index_build_json,
|
|
67
|
+
assert_index_status_json,
|
|
68
|
+
assert_index_verify_json,
|
|
64
69
|
assert_install_doctor_lifecycle_output,
|
|
65
70
|
assert_list_catalog_output,
|
|
66
71
|
assert_list_catalog_json,
|
|
@@ -72,6 +77,8 @@ from smoke_assertions import (
|
|
|
72
77
|
assert_pack_json_component_spec,
|
|
73
78
|
assert_pack_markdown_body_component_spec,
|
|
74
79
|
assert_pack_markdown_component_spec,
|
|
80
|
+
assert_ranked_search_determinism,
|
|
81
|
+
assert_ranked_search_json,
|
|
75
82
|
assert_site_repair_apply_report_payload,
|
|
76
83
|
assert_site_repair_guidance_report_contract,
|
|
77
84
|
assert_site_repair_preview_report_payload,
|
|
@@ -132,6 +139,10 @@ from smoke_assertions import (
|
|
|
132
139
|
parse_help_topics,
|
|
133
140
|
passing_doctor_report_json,
|
|
134
141
|
passing_check_artifact_content,
|
|
142
|
+
passing_index_build_json,
|
|
143
|
+
passing_index_status_json,
|
|
144
|
+
passing_index_verify_json,
|
|
145
|
+
passing_ranked_search_json,
|
|
135
146
|
passing_site_json,
|
|
136
147
|
passing_site_mcp_check_json,
|
|
137
148
|
passing_site_mcp_check_probes_human,
|
|
@@ -2205,6 +2216,102 @@ def assert_learning_redact_smoke(
|
|
|
2205
2216
|
)
|
|
2206
2217
|
|
|
2207
2218
|
|
|
2219
|
+
def assert_learning_recall_json(
|
|
2220
|
+
raw: str,
|
|
2221
|
+
*,
|
|
2222
|
+
context: str,
|
|
2223
|
+
cmd: list[str],
|
|
2224
|
+
) -> None:
|
|
2225
|
+
assert_no_ansi(raw, cmd)
|
|
2226
|
+
try:
|
|
2227
|
+
payload = json.loads(raw)
|
|
2228
|
+
except json.JSONDecodeError as error:
|
|
2229
|
+
raise SystemExit(f"{context}: failed to parse learn recall JSON") from error
|
|
2230
|
+
|
|
2231
|
+
require_registry_smoke(
|
|
2232
|
+
isinstance(payload, dict) and payload.get("query") == "korean mobile",
|
|
2233
|
+
context=context,
|
|
2234
|
+
cmd=cmd,
|
|
2235
|
+
message="learn recall JSON must echo the query",
|
|
2236
|
+
)
|
|
2237
|
+
corpus = payload.get("corpus")
|
|
2238
|
+
require_registry_smoke(
|
|
2239
|
+
isinstance(corpus, dict)
|
|
2240
|
+
and isinstance(corpus.get("candidateCount"), int)
|
|
2241
|
+
and corpus.get("candidateCount") > 0
|
|
2242
|
+
and isinstance(corpus.get("selectedCount"), int)
|
|
2243
|
+
and isinstance(corpus.get("selected"), list),
|
|
2244
|
+
context=context,
|
|
2245
|
+
cmd=cmd,
|
|
2246
|
+
message="learn recall corpus block shape changed",
|
|
2247
|
+
)
|
|
2248
|
+
learning = payload.get("learning")
|
|
2249
|
+
require_registry_smoke(
|
|
2250
|
+
isinstance(learning, dict)
|
|
2251
|
+
and learning.get("mode") == "brief-relevance"
|
|
2252
|
+
and learning.get("candidateCount") == 3
|
|
2253
|
+
and isinstance(learning.get("selected"), list),
|
|
2254
|
+
context=context,
|
|
2255
|
+
cmd=cmd,
|
|
2256
|
+
message="learn recall learning block shape changed",
|
|
2257
|
+
)
|
|
2258
|
+
selected = learning.get("selected")
|
|
2259
|
+
require_registry_smoke(
|
|
2260
|
+
len(selected) >= 1
|
|
2261
|
+
and isinstance(selected[0], dict)
|
|
2262
|
+
and selected[0].get("id") == "registry-korean"
|
|
2263
|
+
and selected[0].get("category") == "korean"
|
|
2264
|
+
and isinstance(selected[0].get("matchedTokens"), list),
|
|
2265
|
+
context=context,
|
|
2266
|
+
cmd=cmd,
|
|
2267
|
+
message="learn recall should rank the Korean learning entry for a Korean query",
|
|
2268
|
+
)
|
|
2269
|
+
|
|
2270
|
+
|
|
2271
|
+
def assert_learning_recall_smoke(
|
|
2272
|
+
command_factory,
|
|
2273
|
+
profile_path: Path,
|
|
2274
|
+
*,
|
|
2275
|
+
env: dict[str, str],
|
|
2276
|
+
cwd: Path | None = None,
|
|
2277
|
+
context: str,
|
|
2278
|
+
) -> None:
|
|
2279
|
+
write_learning_stats_fixture(profile_path)
|
|
2280
|
+
|
|
2281
|
+
json_cmd = command_factory(
|
|
2282
|
+
"learn",
|
|
2283
|
+
"--recall",
|
|
2284
|
+
"korean mobile",
|
|
2285
|
+
"--file",
|
|
2286
|
+
str(profile_path),
|
|
2287
|
+
"--json",
|
|
2288
|
+
)
|
|
2289
|
+
json_result = run_plain(json_cmd, cwd=cwd, env=env)
|
|
2290
|
+
assert_learning_recall_json(json_result.stdout, context=f"{context} JSON", cmd=json_cmd)
|
|
2291
|
+
|
|
2292
|
+
# Read-only: the profile file is unchanged after recall.
|
|
2293
|
+
before = write_learning_stats_fixture_snapshot(profile_path)
|
|
2294
|
+
human_cmd = command_factory("learn", "--recall", "korean mobile", "--file", str(profile_path))
|
|
2295
|
+
human_result = run_plain(human_cmd, cwd=cwd, env=env)
|
|
2296
|
+
require_registry_smoke(
|
|
2297
|
+
"Recall (read-only)" in human_result.stdout
|
|
2298
|
+
and "Privacy: recall is read-only" in human_result.stdout,
|
|
2299
|
+
context=f"{context} human",
|
|
2300
|
+
cmd=human_cmd,
|
|
2301
|
+
message="learn recall human output missing read-only markers",
|
|
2302
|
+
)
|
|
2303
|
+
require_registry_smoke(
|
|
2304
|
+
profile_path.read_text(encoding="utf-8") == before,
|
|
2305
|
+
context=f"{context} read-only",
|
|
2306
|
+
cmd=human_cmd,
|
|
2307
|
+
message="learn recall must not mutate the learning profile",
|
|
2308
|
+
)
|
|
2309
|
+
|
|
2310
|
+
|
|
2311
|
+
def write_learning_stats_fixture_snapshot(profile_path: Path) -> str:
|
|
2312
|
+
return profile_path.read_text(encoding="utf-8")
|
|
2313
|
+
|
|
2314
|
+
|
|
2208
2315
|
def assert_learning_stats_json(
|
|
2209
2316
|
raw: str,
|
|
2210
2317
|
*,
|
|
@@ -4412,6 +4519,56 @@ def assert_pack_stdin_smoke(
|
|
|
4412
4519
|
)
|
|
4413
4520
|
|
|
4414
4521
|
|
|
4522
|
+
def assert_recall_context(payload: dict[str, object], *, context: str, cmd: list[str]) -> None:
|
|
4523
|
+
recall = payload.get("recall")
|
|
4524
|
+
require_registry_smoke(
|
|
4525
|
+
isinstance(recall, dict),
|
|
4526
|
+
context=context,
|
|
4527
|
+
cmd=cmd,
|
|
4528
|
+
message="recall should be present when --with-recall is used",
|
|
4529
|
+
)
|
|
4530
|
+
require_registry_smoke(
|
|
4531
|
+
recall.get("mode") == "lexical",
|
|
4532
|
+
context=context,
|
|
4533
|
+
cmd=cmd,
|
|
4534
|
+
message="recall should use the deterministic lexical scorer",
|
|
4535
|
+
)
|
|
4536
|
+
selected_count = recall.get("selectedCount")
|
|
4537
|
+
require_registry_smoke(
|
|
4538
|
+
isinstance(selected_count, int) and selected_count >= 1,
|
|
4539
|
+
context=context,
|
|
4540
|
+
cmd=cmd,
|
|
4541
|
+
message="recall should select at least one corpus file for the Button brief",
|
|
4542
|
+
)
|
|
4543
|
+
selected = recall.get("selected")
|
|
4544
|
+
require_registry_smoke(
|
|
4545
|
+
isinstance(selected, list) and len(selected) == selected_count and len(selected) >= 1,
|
|
4546
|
+
context=context,
|
|
4547
|
+
cmd=cmd,
|
|
4548
|
+
message="recall selected list should match the reported selected count",
|
|
4549
|
+
)
|
|
4550
|
+
top = selected[0]
|
|
4551
|
+
require_registry_smoke(
|
|
4552
|
+
isinstance(top, dict) and isinstance(top.get("id"), str) and top.get("id"),
|
|
4553
|
+
context=context,
|
|
4554
|
+
cmd=cmd,
|
|
4555
|
+
message="recall selection should cite the corpus relPath as its id",
|
|
4556
|
+
)
|
|
4557
|
+
require_registry_smoke(
|
|
4558
|
+
type(top.get("score")) in (int, float) and top.get("score") > 0,
|
|
4559
|
+
context=context,
|
|
4560
|
+
cmd=cmd,
|
|
4561
|
+
message="recall selection should include a positive relevance score",
|
|
4562
|
+
)
|
|
4563
|
+
markdown = recall.get("markdown")
|
|
4564
|
+
require_registry_smoke(
|
|
4565
|
+
isinstance(markdown, str) and "## Recalled design knowledge" in markdown and top.get("id") in markdown,
|
|
4566
|
+
context=context,
|
|
4567
|
+
cmd=cmd,
|
|
4568
|
+
message="recall markdown should carry the Recalled design knowledge section and cite the top file",
|
|
4569
|
+
)
|
|
4570
|
+
|
|
4571
|
+
|
|
4415
4572
|
def assert_learning_relevance_context(payload: dict[str, object], *, context: str, cmd: list[str]) -> None:
|
|
4416
4573
|
learning_context = payload.get("learningContext")
|
|
4417
4574
|
require_registry_smoke(
|
|
@@ -4989,6 +5146,49 @@ def assert_learning_relevance_smoke(
|
|
|
4989
5146
|
require_registry_smoke(isinstance(plan, dict), context=f"{context} pack", cmd=pack_cmd, message="pack plan missing")
|
|
4990
5147
|
assert_learning_relevance_context(plan, context=f"{context} pack plan", cmd=pack_cmd)
|
|
4991
5148
|
|
|
5149
|
+
recall_prompt_cmd = command_factory(
|
|
5150
|
+
"prompt",
|
|
5151
|
+
EXPECTED_ROUTE_BRIEF,
|
|
5152
|
+
"--route",
|
|
5153
|
+
EXPECTED_ROUTE_ID,
|
|
5154
|
+
"--with-recall",
|
|
5155
|
+
"--recall-limit",
|
|
5156
|
+
"3",
|
|
5157
|
+
"--json",
|
|
5158
|
+
)
|
|
5159
|
+
recall_prompt_result = run_plain(recall_prompt_cmd, cwd=cwd, env=relevance_env)
|
|
5160
|
+
try:
|
|
5161
|
+
recall_prompt_payload = json.loads(recall_prompt_result.stdout)
|
|
5162
|
+
except json.JSONDecodeError as error:
|
|
5163
|
+
raise SystemExit(f"{context}: failed to parse prompt recall JSON") from error
|
|
5164
|
+
assert_recall_context(recall_prompt_payload, context=f"{context} prompt recall", cmd=recall_prompt_cmd)
|
|
5165
|
+
|
|
5166
|
+
recall_pack_cmd = command_factory(
|
|
5167
|
+
"pack",
|
|
5168
|
+
EXPECTED_ROUTE_BRIEF,
|
|
5169
|
+
"--route",
|
|
5170
|
+
EXPECTED_ROUTE_ID,
|
|
5171
|
+
"--with-recall",
|
|
5172
|
+
"--recall-limit",
|
|
5173
|
+
"3",
|
|
5174
|
+
"--max-bytes",
|
|
5175
|
+
str(EXPECTED_PACK_MAX_BYTES),
|
|
5176
|
+
"--json",
|
|
5177
|
+
)
|
|
5178
|
+
recall_pack_result = run_plain(recall_pack_cmd, cwd=cwd, env=relevance_env)
|
|
5179
|
+
try:
|
|
5180
|
+
recall_pack_payload = json.loads(recall_pack_result.stdout)
|
|
5181
|
+
except json.JSONDecodeError as error:
|
|
5182
|
+
raise SystemExit(f"{context}: failed to parse pack recall JSON") from error
|
|
5183
|
+
recall_plan = recall_pack_payload.get("plan")
|
|
5184
|
+
require_registry_smoke(
|
|
5185
|
+
isinstance(recall_plan, dict),
|
|
5186
|
+
context=f"{context} pack recall",
|
|
5187
|
+
cmd=recall_pack_cmd,
|
|
5188
|
+
message="pack recall plan missing",
|
|
5189
|
+
)
|
|
5190
|
+
assert_recall_context(recall_plan, context=f"{context} pack recall plan", cmd=recall_pack_cmd)
|
|
5191
|
+
|
|
4992
5192
|
eval_template_path = profile_path.with_name(f"{profile_path.stem}-eval-template.json")
|
|
4993
5193
|
eval_template_path.write_text("stale eval template output\n", encoding="utf-8")
|
|
4994
5194
|
eval_template_cmd = command_factory(
|
|
@@ -5055,6 +5255,166 @@ def assert_learning_relevance_smoke(
|
|
|
5055
5255
|
)
|
|
5056
5256
|
|
|
5057
5257
|
|
|
5258
|
+
def assert_index_roundtrip_smoke(
|
|
5259
|
+
command_factory,
|
|
5260
|
+
index_dir: Path,
|
|
5261
|
+
profile_path: Path,
|
|
5262
|
+
*,
|
|
5263
|
+
env: dict[str, str],
|
|
5264
|
+
cwd: Path | None = None,
|
|
5265
|
+
context: str,
|
|
5266
|
+
) -> None:
|
|
5267
|
+
"""`index --build` -> `index --status` -> `index --verify` round-trip against a
|
|
5268
|
+
temp index dir and learning profile, isolated from the ambient environment."""
|
|
5269
|
+
index_dir.mkdir(parents=True, exist_ok=True)
|
|
5270
|
+
write_learning_relevance_fixture(profile_path)
|
|
5271
|
+
index_env = env.copy()
|
|
5272
|
+
index_env["DESIGN_AI_INDEX_DIR"] = str(index_dir)
|
|
5273
|
+
index_env["DESIGN_AI_LEARNING_FILE"] = str(profile_path)
|
|
5274
|
+
|
|
5275
|
+
build_cmd = command_factory("index", "--build", "--json")
|
|
5276
|
+
build_result = run_plain(build_cmd, cwd=cwd, env=index_env)
|
|
5277
|
+
assert_index_build_json(
|
|
5278
|
+
build_result.stdout,
|
|
5279
|
+
index_dir=str(index_dir),
|
|
5280
|
+
context=f"{context} build",
|
|
5281
|
+
cmd=build_cmd,
|
|
5282
|
+
)
|
|
5283
|
+
|
|
5284
|
+
status_cmd = command_factory("index", "--status", "--json")
|
|
5285
|
+
status_result = run_plain(status_cmd, cwd=cwd, env=index_env)
|
|
5286
|
+
assert_index_status_json(
|
|
5287
|
+
status_result.stdout,
|
|
5288
|
+
index_dir=str(index_dir),
|
|
5289
|
+
context=f"{context} status",
|
|
5290
|
+
cmd=status_cmd,
|
|
5291
|
+
)
|
|
5292
|
+
|
|
5293
|
+
verify_cmd = command_factory("index", "--verify", "--json")
|
|
5294
|
+
verify_result = run_plain(verify_cmd, cwd=cwd, env=index_env)
|
|
5295
|
+
assert_index_verify_json(
|
|
5296
|
+
verify_result.stdout,
|
|
5297
|
+
index_dir=str(index_dir),
|
|
5298
|
+
context=f"{context} verify",
|
|
5299
|
+
cmd=verify_cmd,
|
|
5300
|
+
)
|
|
5301
|
+
|
|
5302
|
+
|
|
5303
|
+
def assert_ranked_search_determinism_smoke(
|
|
5304
|
+
command_factory,
|
|
5305
|
+
index_dir: Path,
|
|
5306
|
+
*,
|
|
5307
|
+
env: dict[str, str],
|
|
5308
|
+
cwd: Path | None = None,
|
|
5309
|
+
context: str,
|
|
5310
|
+
) -> None:
|
|
5311
|
+
"""Two identical `search --ranked --json` runs must produce byte-identical output."""
|
|
5312
|
+
index_dir.mkdir(parents=True, exist_ok=True)
|
|
5313
|
+
ranked_env = env.copy()
|
|
5314
|
+
ranked_env["DESIGN_AI_INDEX_DIR"] = str(index_dir)
|
|
5315
|
+
|
|
5316
|
+
ranked_cmd = command_factory(
|
|
5317
|
+
"search",
|
|
5318
|
+
EXPECTED_CORPUS_SEARCH_QUERY,
|
|
5319
|
+
"--dir",
|
|
5320
|
+
"knowledge",
|
|
5321
|
+
"--limit",
|
|
5322
|
+
str(EXPECTED_RANKED_SEARCH_LIMIT),
|
|
5323
|
+
"--ranked",
|
|
5324
|
+
"--json",
|
|
5325
|
+
)
|
|
5326
|
+
first_result = run_plain(ranked_cmd, cwd=cwd, env=ranked_env)
|
|
5327
|
+
second_result = run_plain(ranked_cmd, cwd=cwd, env=ranked_env)
|
|
5328
|
+
assert_ranked_search_json(
|
|
5329
|
+
first_result.stdout,
|
|
5330
|
+
context=f"{context} payload",
|
|
5331
|
+
cmd=ranked_cmd,
|
|
5332
|
+
expected_notice=EXPECTED_RANKED_SEARCH_NOT_BUILT_NOTICE,
|
|
5333
|
+
)
|
|
5334
|
+
assert_ranked_search_determinism(
|
|
5335
|
+
first_result.stdout,
|
|
5336
|
+
second_result.stdout,
|
|
5337
|
+
context=context,
|
|
5338
|
+
cmd=ranked_cmd,
|
|
5339
|
+
)
|
|
5340
|
+
|
|
5341
|
+
|
|
5342
|
+
def assert_embeddings_off_by_default_smoke(
|
|
5343
|
+
command_factory,
|
|
5344
|
+
index_dir: Path,
|
|
5345
|
+
profile_path: Path,
|
|
5346
|
+
*,
|
|
5347
|
+
env: dict[str, str],
|
|
5348
|
+
cwd: Path | None = None,
|
|
5349
|
+
context: str,
|
|
5350
|
+
) -> None:
|
|
5351
|
+
"""Phase B is opt-in: a plain `index --build` never touches embeddings, and
|
|
5352
|
+
`index --status` reports `embeddings: null` (the feature is simply unused)."""
|
|
5353
|
+
index_dir.mkdir(parents=True, exist_ok=True)
|
|
5354
|
+
write_learning_relevance_fixture(profile_path)
|
|
5355
|
+
index_env = env.copy()
|
|
5356
|
+
index_env["DESIGN_AI_INDEX_DIR"] = str(index_dir)
|
|
5357
|
+
index_env["DESIGN_AI_LEARNING_FILE"] = str(profile_path)
|
|
5358
|
+
# Point at a config file that is guaranteed not to exist, rather than relying on
|
|
5359
|
+
# the ambient environment lacking DESIGN_AI_CONFIG_FILE — a real ~/.design-ai/
|
|
5360
|
+
# config.json on the machine running this smoke must not leak into the test.
|
|
5361
|
+
index_env["DESIGN_AI_CONFIG_FILE"] = str(index_dir.parent / "no-config-here" / "config.json")
|
|
5362
|
+
|
|
5363
|
+
build_cmd = command_factory("index", "--build", "--json")
|
|
5364
|
+
build_result = run_plain(build_cmd, cwd=cwd, env=index_env)
|
|
5365
|
+
build_payload = json.loads(build_result.stdout)
|
|
5366
|
+
if "embeddings" in build_payload:
|
|
5367
|
+
raise SystemExit(f"index build JSON after {context} unexpectedly ran provider execution with plain --build")
|
|
5368
|
+
|
|
5369
|
+
status_cmd = command_factory("index", "--status", "--json")
|
|
5370
|
+
status_result = run_plain(status_cmd, cwd=cwd, env=index_env)
|
|
5371
|
+
assert_index_status_json(
|
|
5372
|
+
status_result.stdout,
|
|
5373
|
+
index_dir=str(index_dir),
|
|
5374
|
+
context=f"{context} status",
|
|
5375
|
+
cmd=status_cmd,
|
|
5376
|
+
expect_embeddings=False,
|
|
5377
|
+
)
|
|
5378
|
+
|
|
5379
|
+
|
|
5380
|
+
def assert_search_embeddings_no_provider_fallback_smoke(
|
|
5381
|
+
command_factory,
|
|
5382
|
+
*,
|
|
5383
|
+
env: dict[str, str],
|
|
5384
|
+
cwd: Path | None = None,
|
|
5385
|
+
context: str,
|
|
5386
|
+
config_file: Path,
|
|
5387
|
+
) -> None:
|
|
5388
|
+
"""`search --ranked --embeddings` with no provider configured (flag or config)
|
|
5389
|
+
degrades to the lexical backend with a notice and exit code 0."""
|
|
5390
|
+
fallback_env = env.copy()
|
|
5391
|
+
# Same isolation rationale as assert_embeddings_off_by_default_smoke: point at a
|
|
5392
|
+
# config path guaranteed not to exist rather than trusting the ambient environment.
|
|
5393
|
+
fallback_env["DESIGN_AI_CONFIG_FILE"] = str(config_file)
|
|
5394
|
+
|
|
5395
|
+
search_cmd = command_factory(
|
|
5396
|
+
"search",
|
|
5397
|
+
EXPECTED_CORPUS_SEARCH_QUERY,
|
|
5398
|
+
"--dir",
|
|
5399
|
+
"knowledge",
|
|
5400
|
+
"--limit",
|
|
5401
|
+
str(EXPECTED_RANKED_SEARCH_LIMIT),
|
|
5402
|
+
"--ranked",
|
|
5403
|
+
"--embeddings",
|
|
5404
|
+
"--json",
|
|
5405
|
+
)
|
|
5406
|
+
result = run_plain(search_cmd, cwd=cwd, env=fallback_env)
|
|
5407
|
+
assert_ranked_search_json(
|
|
5408
|
+
result.stdout,
|
|
5409
|
+
context=f"{context} payload",
|
|
5410
|
+
cmd=search_cmd,
|
|
5411
|
+
expected_backend="lexical",
|
|
5412
|
+
)
|
|
5413
|
+
payload = json.loads(result.stdout)
|
|
5414
|
+
if "embedding provider" not in payload.get("notice", "") and "no embedding provider configured" not in payload.get("notice", ""):
|
|
5415
|
+
raise SystemExit(f"search ranked JSON after {context} fallback notice does not mention the missing provider")
|
|
5416
|
+
|
|
5417
|
+
|
|
5058
5418
|
def wait_for_registry_package(
|
|
5059
5419
|
package_spec: str,
|
|
5060
5420
|
*,
|
|
@@ -6086,6 +6446,13 @@ def smoke_registry_package(package_spec: str, *, retries: int, delay: float) ->
|
|
|
6086
6446
|
env=env,
|
|
6087
6447
|
context="registry smoke npm exec learn stats",
|
|
6088
6448
|
)
|
|
6449
|
+
assert_learning_recall_smoke(
|
|
6450
|
+
lambda *args: npm_exec_cmd(package_spec, *args),
|
|
6451
|
+
npx_root / "registry-recall-learning.json",
|
|
6452
|
+
cwd=npx_root,
|
|
6453
|
+
env=env,
|
|
6454
|
+
context="registry smoke npm exec learn recall",
|
|
6455
|
+
)
|
|
6089
6456
|
assert_learning_audit_cleanup_smoke(
|
|
6090
6457
|
lambda *args: npm_exec_cmd(package_spec, *args),
|
|
6091
6458
|
npx_root / "registry-audit-learning.json",
|
|
@@ -6100,6 +6467,36 @@ def smoke_registry_package(package_spec: str, *, retries: int, delay: float) ->
|
|
|
6100
6467
|
env=env,
|
|
6101
6468
|
context="registry smoke npm exec learning relevance",
|
|
6102
6469
|
)
|
|
6470
|
+
assert_index_roundtrip_smoke(
|
|
6471
|
+
lambda *args: npm_exec_cmd(package_spec, *args),
|
|
6472
|
+
npx_root / "registry-index",
|
|
6473
|
+
npx_root / "registry-index-learning.json",
|
|
6474
|
+
cwd=npx_root,
|
|
6475
|
+
env=env,
|
|
6476
|
+
context="registry smoke npm exec index roundtrip",
|
|
6477
|
+
)
|
|
6478
|
+
assert_ranked_search_determinism_smoke(
|
|
6479
|
+
lambda *args: npm_exec_cmd(package_spec, *args),
|
|
6480
|
+
npx_root / "registry-ranked-search-index",
|
|
6481
|
+
cwd=npx_root,
|
|
6482
|
+
env=env,
|
|
6483
|
+
context="registry smoke npm exec ranked search determinism",
|
|
6484
|
+
)
|
|
6485
|
+
assert_embeddings_off_by_default_smoke(
|
|
6486
|
+
lambda *args: npm_exec_cmd(package_spec, *args),
|
|
6487
|
+
npx_root / "registry-embeddings-off-index",
|
|
6488
|
+
npx_root / "registry-embeddings-off-learning.json",
|
|
6489
|
+
cwd=npx_root,
|
|
6490
|
+
env=env,
|
|
6491
|
+
context="registry smoke npm exec embeddings off by default",
|
|
6492
|
+
)
|
|
6493
|
+
assert_search_embeddings_no_provider_fallback_smoke(
|
|
6494
|
+
lambda *args: npm_exec_cmd(package_spec, *args),
|
|
6495
|
+
cwd=npx_root,
|
|
6496
|
+
env=env,
|
|
6497
|
+
context="registry smoke npm exec search embeddings no provider fallback",
|
|
6498
|
+
config_file=npx_root / "registry-no-config-here" / "config.json",
|
|
6499
|
+
)
|
|
6103
6500
|
assert_update_dry_run_smoke(
|
|
6104
6501
|
npm_exec_cmd(package_spec, "update", "--dry-run"),
|
|
6105
6502
|
cwd=npx_root,
|
|
@@ -8297,6 +8694,38 @@ def run_self_test() -> None:
|
|
|
8297
8694
|
scope="registry smoke",
|
|
8298
8695
|
)
|
|
8299
8696
|
|
|
8697
|
+
recall_payload = {
|
|
8698
|
+
"recall": {
|
|
8699
|
+
"query": EXPECTED_ROUTE_BRIEF,
|
|
8700
|
+
"mode": "lexical",
|
|
8701
|
+
"candidateCount": 42,
|
|
8702
|
+
"selectedCount": 1,
|
|
8703
|
+
"selected": [
|
|
8704
|
+
{
|
|
8705
|
+
"id": "knowledge/components/INDEX.md",
|
|
8706
|
+
"score": 9.5,
|
|
8707
|
+
"matchedTokens": ["button", "component"],
|
|
8708
|
+
},
|
|
8709
|
+
],
|
|
8710
|
+
"markdown": (
|
|
8711
|
+
"## Recalled design knowledge\n\n"
|
|
8712
|
+
"- knowledge/components/INDEX.md\n"
|
|
8713
|
+
" - Component index"
|
|
8714
|
+
),
|
|
8715
|
+
},
|
|
8716
|
+
}
|
|
8717
|
+
recall_cmd = ["design-ai", "prompt", EXPECTED_ROUTE_BRIEF, "--with-recall", "--json"]
|
|
8718
|
+
assert_recall_context(recall_payload, context="registry smoke self-test", cmd=recall_cmd)
|
|
8719
|
+
expect_self_test_failure(
|
|
8720
|
+
lambda: assert_recall_context(
|
|
8721
|
+
{**recall_payload, "recall": {**recall_payload["recall"], "selectedCount": 0, "selected": []}},
|
|
8722
|
+
context="registry smoke self-test",
|
|
8723
|
+
cmd=recall_cmd,
|
|
8724
|
+
),
|
|
8725
|
+
expected="recall should select at least one corpus file",
|
|
8726
|
+
scope="registry smoke",
|
|
8727
|
+
)
|
|
8728
|
+
|
|
8300
8729
|
learning_audit_path = tmp_root / "learning-audit.json"
|
|
8301
8730
|
duplicate_command_args = [
|
|
8302
8731
|
"design-ai",
|
|
@@ -8503,6 +8932,111 @@ def run_self_test() -> None:
|
|
|
8503
8932
|
scope="registry smoke",
|
|
8504
8933
|
)
|
|
8505
8934
|
|
|
8935
|
+
index_dir = str(tmp_root / "registry-index-self-test")
|
|
8936
|
+
index_build_cmd = ["design-ai", "index", "--build", "--json"]
|
|
8937
|
+
assert_index_build_json(
|
|
8938
|
+
passing_index_build_json(index_dir),
|
|
8939
|
+
index_dir=index_dir,
|
|
8940
|
+
context="registry smoke self-test",
|
|
8941
|
+
cmd=index_build_cmd,
|
|
8942
|
+
)
|
|
8943
|
+
expect_self_test_failure(
|
|
8944
|
+
lambda: assert_index_build_json(
|
|
8945
|
+
"{",
|
|
8946
|
+
index_dir=index_dir,
|
|
8947
|
+
context="registry smoke self-test",
|
|
8948
|
+
cmd=index_build_cmd,
|
|
8949
|
+
),
|
|
8950
|
+
expected="failed to parse index build JSON",
|
|
8951
|
+
scope="registry smoke",
|
|
8952
|
+
)
|
|
8953
|
+
|
|
8954
|
+
index_status_cmd = ["design-ai", "index", "--status", "--json"]
|
|
8955
|
+
assert_index_status_json(
|
|
8956
|
+
passing_index_status_json(index_dir),
|
|
8957
|
+
index_dir=index_dir,
|
|
8958
|
+
context="registry smoke self-test",
|
|
8959
|
+
cmd=index_status_cmd,
|
|
8960
|
+
)
|
|
8961
|
+
assert_index_status_json(
|
|
8962
|
+
passing_index_status_json(index_dir, with_embeddings=True),
|
|
8963
|
+
index_dir=index_dir,
|
|
8964
|
+
context="registry smoke self-test embeddings",
|
|
8965
|
+
cmd=index_status_cmd,
|
|
8966
|
+
expect_embeddings=True,
|
|
8967
|
+
)
|
|
8968
|
+
expect_self_test_failure(
|
|
8969
|
+
lambda: assert_index_status_json(
|
|
8970
|
+
passing_index_status_json(index_dir, with_embeddings=True),
|
|
8971
|
+
index_dir=index_dir,
|
|
8972
|
+
context="registry smoke self-test embeddings unexpected",
|
|
8973
|
+
cmd=index_status_cmd,
|
|
8974
|
+
expect_embeddings=False,
|
|
8975
|
+
),
|
|
8976
|
+
expected="embeddings is not null when embeddings were not built",
|
|
8977
|
+
scope="registry smoke",
|
|
8978
|
+
)
|
|
8979
|
+
|
|
8980
|
+
index_verify_cmd = ["design-ai", "index", "--verify", "--json"]
|
|
8981
|
+
assert_index_verify_json(
|
|
8982
|
+
passing_index_verify_json(index_dir),
|
|
8983
|
+
index_dir=index_dir,
|
|
8984
|
+
context="registry smoke self-test",
|
|
8985
|
+
cmd=index_verify_cmd,
|
|
8986
|
+
)
|
|
8987
|
+
expect_self_test_failure(
|
|
8988
|
+
lambda: assert_index_verify_json(
|
|
8989
|
+
json.dumps({**json.loads(passing_index_verify_json(index_dir)), "ok": False}),
|
|
8990
|
+
index_dir=index_dir,
|
|
8991
|
+
context="registry smoke self-test",
|
|
8992
|
+
cmd=index_verify_cmd,
|
|
8993
|
+
),
|
|
8994
|
+
expected="ok is not true",
|
|
8995
|
+
scope="registry smoke",
|
|
8996
|
+
)
|
|
8997
|
+
|
|
8998
|
+
ranked_search_cmd = ["design-ai", "search", EXPECTED_CORPUS_SEARCH_QUERY, "--ranked", "--json"]
|
|
8999
|
+
ranked_search_json = passing_ranked_search_json()
|
|
9000
|
+
assert_ranked_search_json(
|
|
9001
|
+
ranked_search_json,
|
|
9002
|
+
context="registry smoke self-test",
|
|
9003
|
+
cmd=ranked_search_cmd,
|
|
9004
|
+
)
|
|
9005
|
+
assert_ranked_search_determinism(
|
|
9006
|
+
ranked_search_json,
|
|
9007
|
+
ranked_search_json,
|
|
9008
|
+
context="registry smoke self-test",
|
|
9009
|
+
cmd=ranked_search_cmd,
|
|
9010
|
+
)
|
|
9011
|
+
expect_self_test_failure(
|
|
9012
|
+
lambda: assert_ranked_search_determinism(
|
|
9013
|
+
ranked_search_json,
|
|
9014
|
+
passing_ranked_search_json(backend="lexical"),
|
|
9015
|
+
context="registry smoke self-test",
|
|
9016
|
+
cmd=ranked_search_cmd,
|
|
9017
|
+
),
|
|
9018
|
+
expected="differs between identical runs",
|
|
9019
|
+
scope="registry smoke",
|
|
9020
|
+
)
|
|
9021
|
+
|
|
9022
|
+
ranked_search_fallback_json = passing_ranked_search_json(backend="lexical")
|
|
9023
|
+
assert_ranked_search_json(
|
|
9024
|
+
ranked_search_fallback_json,
|
|
9025
|
+
context="registry smoke self-test fallback",
|
|
9026
|
+
cmd=ranked_search_cmd,
|
|
9027
|
+
expected_backend="lexical",
|
|
9028
|
+
)
|
|
9029
|
+
expect_self_test_failure(
|
|
9030
|
+
lambda: assert_ranked_search_json(
|
|
9031
|
+
passing_ranked_search_json(backend="embeddings"),
|
|
9032
|
+
context="registry smoke self-test fallback mismatch",
|
|
9033
|
+
cmd=ranked_search_cmd,
|
|
9034
|
+
expected_backend="lexical",
|
|
9035
|
+
),
|
|
9036
|
+
expected="backend differs from expected backend",
|
|
9037
|
+
scope="registry smoke",
|
|
9038
|
+
)
|
|
9039
|
+
|
|
8506
9040
|
expect_self_test_failure(
|
|
8507
9041
|
lambda: smoke_registry_package("self-test-package", retries=0, delay=0),
|
|
8508
9042
|
expected="--retries must be at least 1",
|
|
@@ -38,6 +38,10 @@ REQUIRED_RELEASE_POLICY_DOC_LABELS = (
|
|
|
38
38
|
RELEASE_POLICY_DOC_PATHS = tuple(
|
|
39
39
|
(label, ROOT / label) for label in REQUIRED_RELEASE_POLICY_DOC_LABELS
|
|
40
40
|
)
|
|
41
|
+
RELEASE_POLICY_COMPANION_DOC_PATHS: dict[str, Path] = {
|
|
42
|
+
"README.md": ROOT / "docs" / "RELEASE-GATES.md",
|
|
43
|
+
"README.ko.md": ROOT / "docs" / "RELEASE-GATES.ko.md",
|
|
44
|
+
}
|
|
41
45
|
PRODUCT_READINESS_WARNING_STRICT_COMPARE_TERM_GROUPS = (
|
|
42
46
|
(
|
|
43
47
|
"warning-state Website Console bundle-compare strict smoke coverage",
|
|
@@ -3837,16 +3841,32 @@ def load_text_input(label: str, path: Path) -> tuple[str, list[str]]:
|
|
|
3837
3841
|
|
|
3838
3842
|
def load_release_policy_docs(
|
|
3839
3843
|
policy_doc_paths: tuple[tuple[str, Path], ...] = RELEASE_POLICY_DOC_PATHS,
|
|
3844
|
+
companion_doc_paths: dict[str, Path] | None = None,
|
|
3840
3845
|
) -> tuple[dict[str, str], list[str]]:
|
|
3846
|
+
if companion_doc_paths is None:
|
|
3847
|
+
companion_doc_paths = RELEASE_POLICY_COMPANION_DOC_PATHS
|
|
3841
3848
|
docs: dict[str, str] = {}
|
|
3842
3849
|
errors: list[str] = []
|
|
3843
3850
|
for label, path in policy_doc_paths:
|
|
3844
3851
|
try:
|
|
3845
|
-
|
|
3852
|
+
text = path.read_text(encoding="utf-8")
|
|
3846
3853
|
except FileNotFoundError:
|
|
3847
3854
|
errors.append(f"release policy docs required file is missing on disk: {label} ({path})")
|
|
3855
|
+
continue
|
|
3848
3856
|
except OSError as exc:
|
|
3849
3857
|
errors.append(f"release policy docs required file cannot be read: {label} ({path}): {exc}")
|
|
3858
|
+
continue
|
|
3859
|
+
companion_path = companion_doc_paths.get(label)
|
|
3860
|
+
if companion_path is not None and companion_path.exists():
|
|
3861
|
+
try:
|
|
3862
|
+
companion_text = companion_path.read_text(encoding="utf-8")
|
|
3863
|
+
except OSError as exc:
|
|
3864
|
+
errors.append(
|
|
3865
|
+
f"release policy companion doc cannot be read: {label} ({companion_path}): {exc}"
|
|
3866
|
+
)
|
|
3867
|
+
else:
|
|
3868
|
+
text = f"{text}\n\n{companion_text}"
|
|
3869
|
+
docs[label] = text
|
|
3850
3870
|
return docs, errors
|
|
3851
3871
|
|
|
3852
3872
|
|
|
@@ -9736,6 +9756,32 @@ class AuditResult:
|
|
|
9736
9756
|
"release policy doc loader should report missing files without a traceback",
|
|
9737
9757
|
)
|
|
9738
9758
|
|
|
9759
|
+
with tempfile.TemporaryDirectory() as companion_temp_dir:
|
|
9760
|
+
companion_root = Path(companion_temp_dir)
|
|
9761
|
+
base_doc = companion_root / "README.md"
|
|
9762
|
+
base_doc.write_text("summary only\n", encoding="utf-8")
|
|
9763
|
+
companion_doc = companion_root / "RELEASE-GATES.md"
|
|
9764
|
+
companion_doc.write_text("companion evidence phrase\n", encoding="utf-8")
|
|
9765
|
+
companion_docs, companion_errors = load_release_policy_docs(
|
|
9766
|
+
(("README.md", base_doc),),
|
|
9767
|
+
{"README.md": companion_doc},
|
|
9768
|
+
)
|
|
9769
|
+
assert_condition(
|
|
9770
|
+
companion_errors == []
|
|
9771
|
+
and "summary only" in companion_docs["README.md"]
|
|
9772
|
+
and "companion evidence phrase" in companion_docs["README.md"],
|
|
9773
|
+
"release policy doc loader should append companion evidence docs to their base doc",
|
|
9774
|
+
)
|
|
9775
|
+
missing_companion_docs, missing_companion_errors = load_release_policy_docs(
|
|
9776
|
+
(("README.md", base_doc),),
|
|
9777
|
+
{"README.md": companion_root / "MISSING-GATES.md"},
|
|
9778
|
+
)
|
|
9779
|
+
assert_condition(
|
|
9780
|
+
missing_companion_errors == []
|
|
9781
|
+
and missing_companion_docs["README.md"] == "summary only\n",
|
|
9782
|
+
"release policy doc loader should treat a missing companion doc as optional",
|
|
9783
|
+
)
|
|
9784
|
+
|
|
9739
9785
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
9740
9786
|
temp_path = Path(temp_dir)
|
|
9741
9787
|
valid_json = temp_path / "valid.json"
|