@design-ai/cli 4.55.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 +75 -0
- package/README.ko.md +15 -32
- package/README.md +18 -31
- 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 +221 -13
- 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-analysis.mjs +297 -0
- package/cli/lib/site-args.mjs +433 -0
- package/cli/lib/site-bundle-build.mjs +127 -0
- package/cli/lib/site-bundle-check.mjs +454 -0
- package/cli/lib/site-bundle-commands.mjs +95 -0
- package/cli/lib/site-bundle-compare.mjs +157 -0
- package/cli/lib/site-bundle-contract.mjs +79 -0
- package/cli/lib/site-bundle-files.mjs +87 -0
- 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-actions.mjs +113 -0
- package/cli/lib/site-bundle-handoff-runbook-evidence-fields.mjs +164 -0
- package/cli/lib/site-bundle-handoff-runbook-evidence.mjs +334 -0
- package/cli/lib/site-bundle-handoff-runbook-format.mjs +31 -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-stage-metadata.mjs +84 -0
- package/cli/lib/site-bundle-handoff-runbook.mjs +444 -0
- package/cli/lib/site-bundle-handoff-summary.mjs +183 -0
- package/cli/lib/site-bundle-handoff.mjs +271 -0
- package/cli/lib/site-bundle-readme.mjs +98 -0
- package/cli/lib/site-bundle-repair-report.mjs +143 -0
- package/cli/lib/site-bundle-repair.mjs +68 -0
- package/cli/lib/site-content.mjs +399 -0
- package/cli/lib/site-evidence.mjs +35 -0
- package/cli/lib/site-mcp-commands.mjs +28 -0
- package/cli/lib/site-mcp-probes.mjs +159 -0
- package/cli/lib/site-mcp-readiness.mjs +157 -0
- package/cli/lib/site-mcp-report.mjs +324 -0
- package/cli/lib/site-next-actions.mjs +333 -0
- package/cli/lib/site-options.mjs +104 -0
- package/cli/lib/site-prompts.mjs +332 -0
- package/cli/lib/site-starter.mjs +153 -0
- package/cli/lib/site-strings.mjs +23 -0
- package/cli/lib/site-tasks.mjs +93 -0
- package/cli/lib/site-test-support.mjs +68 -0
- package/cli/lib/site-workflow-graph.mjs +309 -0
- package/cli/lib/site-workspace.mjs +492 -0
- package/cli/lib/site.mjs +108 -6617
- 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 +35 -6
- package/docs/DISTRIBUTION.md +35 -8
- package/docs/NEXT-SURFACE-DECISION.md +125 -0
- package/docs/PRODUCT-READINESS.md +3 -3
- package/docs/RELEASE-CHECKLIST.md +20 -3
- package/docs/RELEASE-GATES.ko.md +35 -0
- package/docs/RELEASE-GATES.md +234 -0
- package/docs/ROADMAP.md +2232 -8
- package/docs/external-status.md +20 -7
- package/docs/inspection-20260630.md +169 -0
- package/docs/integrations/design-ai-mcp-server.md +35 -3
- package/docs/integrations/vscode-walkthrough.ko.md +3 -3
- package/docs/integrations/vscode-walkthrough.md +3 -3
- 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/docs/site-overrides/main.html +1 -1
- 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 +717 -9
- package/tools/audit/registry-smoke.py +912 -10
- package/tools/audit/release-metadata.py +47 -1
- package/tools/audit/smoke_assertions.py +998 -35
- package/tools/migrations/refs-links-to-reference-pages.py +179 -0
|
@@ -14,6 +14,7 @@ from doctor_assertions import EXPECTED_DOCTOR_PASS_LABELS, assert_doctor_report_
|
|
|
14
14
|
|
|
15
15
|
ANSI_ESCAPE_RE = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]")
|
|
16
16
|
ROOT = Path(__file__).resolve().parents[2]
|
|
17
|
+
PACKAGE_JSON = ROOT / "package.json"
|
|
17
18
|
PLUGIN_MANIFEST = ROOT / ".claude-plugin" / "plugin.json"
|
|
18
19
|
PLUGIN_INVENTORY_SECTIONS = (
|
|
19
20
|
("skills", "skill"),
|
|
@@ -21,6 +22,7 @@ PLUGIN_INVENTORY_SECTIONS = (
|
|
|
21
22
|
("agents", "agent"),
|
|
22
23
|
)
|
|
23
24
|
OUTPUT_FORCE_OVERWRITE_SENTINEL = "__design-ai-smoke-force-overwrite-sentinel__"
|
|
25
|
+
EXPECTED_MCP_INVALID_ARGUMENT_MESSAGE = "design_ai_search.limit must be an integer"
|
|
24
26
|
|
|
25
27
|
|
|
26
28
|
def load_plugin_manifest() -> dict[str, object]:
|
|
@@ -30,11 +32,70 @@ def load_plugin_manifest() -> dict[str, object]:
|
|
|
30
32
|
raise SystemExit(f"failed to load plugin manifest for smoke assertions: {PLUGIN_MANIFEST}") from error
|
|
31
33
|
|
|
32
34
|
|
|
35
|
+
def load_package_json() -> dict[str, object]:
|
|
36
|
+
try:
|
|
37
|
+
return json.loads(PACKAGE_JSON.read_text(encoding="utf-8"))
|
|
38
|
+
except (OSError, json.JSONDecodeError) as error:
|
|
39
|
+
raise SystemExit(f"failed to load package metadata for smoke assertions: {PACKAGE_JSON}") from error
|
|
40
|
+
|
|
41
|
+
|
|
33
42
|
def count_manifest_section(manifest: dict[str, object], section: str) -> int:
|
|
34
43
|
items = manifest.get(section)
|
|
35
44
|
return len(items) if isinstance(items, list) else 0
|
|
36
45
|
|
|
37
46
|
|
|
47
|
+
EXPECTED_RELEASE_VERSION = str(load_package_json().get("version", ""))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def mcp_smoke_input() -> str:
|
|
51
|
+
messages = [
|
|
52
|
+
{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-11-25"}},
|
|
53
|
+
{"jsonrpc": "2.0", "method": "notifications/initialized"},
|
|
54
|
+
{"jsonrpc": "2.0", "id": 2, "method": "tools/list"},
|
|
55
|
+
{
|
|
56
|
+
"jsonrpc": "2.0",
|
|
57
|
+
"id": 3,
|
|
58
|
+
"method": "tools/call",
|
|
59
|
+
"params": {
|
|
60
|
+
"name": "design_ai_search",
|
|
61
|
+
"arguments": {"query": "Pretendard", "limit": "10"},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
]
|
|
65
|
+
return "".join(f"{json.dumps(message, separators=(',', ':'))}\n" for message in messages)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def assert_design_ai_mcp_protocol_responses(responses: list[object], *, context: str, cmd: list[str]) -> None:
|
|
69
|
+
by_id = {
|
|
70
|
+
response.get("id"): response
|
|
71
|
+
for response in responses
|
|
72
|
+
if isinstance(response, dict) and "id" in response
|
|
73
|
+
}
|
|
74
|
+
init = by_id.get(1)
|
|
75
|
+
tools = by_id.get(2)
|
|
76
|
+
invalid_call = by_id.get(3)
|
|
77
|
+
|
|
78
|
+
if not (isinstance(init, dict) and init.get("result", {}).get("serverInfo", {}).get("name") == "design-ai"):
|
|
79
|
+
raise SystemExit(f"{context}: MCP initialize response missing design-ai serverInfo")
|
|
80
|
+
|
|
81
|
+
tool_names = [
|
|
82
|
+
item.get("name")
|
|
83
|
+
for item in tools.get("result", {}).get("tools", [])
|
|
84
|
+
] if isinstance(tools, dict) else []
|
|
85
|
+
if "design_ai_route" not in tool_names or "design_ai_search" not in tool_names:
|
|
86
|
+
raise SystemExit(f"{context}: MCP tools/list response missing design-ai tools")
|
|
87
|
+
|
|
88
|
+
invalid_error = invalid_call.get("error", {}) if isinstance(invalid_call, dict) else {}
|
|
89
|
+
invalid_text = str(invalid_error.get("message", "")) if isinstance(invalid_error, dict) else ""
|
|
90
|
+
if not (
|
|
91
|
+
isinstance(invalid_call, dict)
|
|
92
|
+
and isinstance(invalid_error, dict)
|
|
93
|
+
and invalid_error.get("code") == -32602
|
|
94
|
+
and EXPECTED_MCP_INVALID_ARGUMENT_MESSAGE in invalid_text
|
|
95
|
+
):
|
|
96
|
+
raise SystemExit(f"{context}: MCP invalid argument response did not preserve invalid params validation")
|
|
97
|
+
|
|
98
|
+
|
|
38
99
|
def format_inventory_count(count: int, singular: str) -> str:
|
|
39
100
|
return f"{count} {singular}{'' if count == 1 else 's'}"
|
|
40
101
|
|
|
@@ -54,6 +115,7 @@ EXPECTED_HELP_TOPICS = (
|
|
|
54
115
|
"status",
|
|
55
116
|
"list",
|
|
56
117
|
"search",
|
|
118
|
+
"index",
|
|
57
119
|
"show",
|
|
58
120
|
"route",
|
|
59
121
|
"routes",
|
|
@@ -98,17 +160,18 @@ EXPECTED_HELP_TOPIC_USAGES = {
|
|
|
98
160
|
"uninstall": "design-ai uninstall [--json]",
|
|
99
161
|
"status": "design-ai status [--json]",
|
|
100
162
|
"list": "design-ai list [skills|commands|agents] [--json]",
|
|
101
|
-
"search": "design-ai search <query> [--dir kind] [--limit N] [--json]",
|
|
163
|
+
"search": "design-ai search <query> [--dir kind] [--limit N] [--ranked] [--embeddings [--provider \"cmd args\"]] [--json]",
|
|
164
|
+
"index": "design-ai index [--build|--status|--verify] [--json] [--embeddings [--provider \"cmd args\"]]",
|
|
102
165
|
"show": "design-ai show <file[:line]> [--lines N:M] [--context N] [--json]",
|
|
103
166
|
"route": "design-ai route <brief|--from-file file|--stdin|--list|--eval-template|--eval> [--limit N]",
|
|
104
167
|
"routes": "design-ai routes [--json]",
|
|
105
|
-
"prompt": "design-ai prompt <brief|--from-file file|--stdin|--eval-template|--eval> [--route id] [--with-learning] [--learning-category kind] [--learning-limit N] [--out file]",
|
|
106
|
-
"pack": "design-ai pack <brief|--from-file file|--stdin|--eval-template|--eval> [--route id] [--with-learning] [--learning-category kind] [--learning-limit N] [--max-bytes N]",
|
|
168
|
+
"prompt": "design-ai prompt <brief|--from-file file|--stdin|--eval-template|--eval> [--route id] [--with-learning] [--learning-category kind] [--learning-limit N] [--with-recall] [--recall-limit N] [--out file]",
|
|
169
|
+
"pack": "design-ai pack <brief|--from-file file|--stdin|--eval-template|--eval> [--route id] [--with-learning] [--learning-category kind] [--learning-limit N] [--with-recall] [--recall-limit N] [--max-bytes N]",
|
|
107
170
|
"check": "design-ai check <artifact.md|--stdin|--examples> [--route id|--all-routes] [--learn]",
|
|
108
171
|
"audit": "design-ai audit [--strict] [--quiet] [--json]",
|
|
109
172
|
"doctor": "design-ai doctor [--strict] [--json] [--fix]",
|
|
110
173
|
"examples": "design-ai examples [query] [--route id] [--limit N] [--json]",
|
|
111
|
-
"learn": "design-ai learn [--init|--remember text|--feedback text|--list|--export|--query text|--explain|--backup|--redact|--verify|--diff|--restore|--restore-backups [--prune]|--import|--audit [--fix]|--curate|--stats|--usage|--signals [--strict]|--agent-backlog [--strict]|--propose-skills [--min-evidence N] [--review-file path] [--review-check|--apply-plan] [--strict]|--eval-template|--eval [--strict]|--forget id|--clear] [--json|--report|--patch|--review-template] [--out file]",
|
|
174
|
+
"learn": "design-ai learn [--init|--remember text|--feedback text|--list|--export|--query text|--explain|--recall query|--backup|--redact|--verify|--diff|--restore|--restore-backups [--prune]|--import|--audit [--fix]|--curate|--stats|--usage|--signals [--strict]|--agent-backlog [--strict]|--propose-skills [--min-evidence N] [--review-file path] [--review-check|--apply-plan] [--strict]|--eval-template|--eval [--strict]|--forget id|--clear] [--json|--report|--patch|--review-template] [--out file]",
|
|
112
175
|
"workspace": "design-ai workspace [--root path] [--learning-file path] [--learning-usage path] [--learning-eval path] [--strict] [--json]",
|
|
113
176
|
"site": "design-ai site <workspace.json|--stdin> [--strict] [--json|--mcp-check [--probes]|--mcp-plan [--probes] [--json]|--next-actions [--json]|--graph|--tasks|--bundle|--report|--prompts|--prompt id [--task id]] [--out file] | site <bundle-dir> --bundle-check [--json] | site <bundle-dir> --bundle-compare other-bundle-dir [--json] | site <bundle-dir> --bundle-handoff [--task id] [--json] | site <bundle-dir> --bundle-repair [--yes] [--json] [--out file] | site --init --name name --live-url url [--next-actions] [--out file] | site --init --name name --live-url url --bundle --out dir | site --from-intake file.md|--stdin [--json|--next-actions [--json]|--tasks|--bundle [--tasks] --out dir] [--out file] | site --intake-template [--language en|ko] [--json] [--out file] | site --sample [--out file] | site --prompt-list [--json]",
|
|
114
177
|
"mcp": "design-ai mcp",
|
|
@@ -121,19 +184,20 @@ EXPECTED_HELP_TOPIC_FRAGMENTS = {
|
|
|
121
184
|
"uninstall": ("Usage:", "design-ai uninstall [--json]"),
|
|
122
185
|
"status": ("Usage:", "design-ai status [--json]"),
|
|
123
186
|
"list": ("Usage:", "design-ai list [skills|commands|agents]"),
|
|
124
|
-
"search": ("Usage:", "design-ai search <query> [--limit N] [--dir kind] [--json]"),
|
|
187
|
+
"search": ("Usage:", "design-ai search <query> [--limit N] [--dir kind] [--ranked] [--embeddings [--provider \"cmd args\"]] [--json]"),
|
|
188
|
+
"index": ("Usage:", "design-ai index [--build|--status|--verify] [--json] [--embeddings [--provider \"cmd args\"]]"),
|
|
125
189
|
"show": ("Usage:", "design-ai show <file[:line|start-end]> [--lines N:M] [--context N] [--json]"),
|
|
126
190
|
"route": ("Usage:", "design-ai route <brief>", "design-ai route --list [--json]", "design-ai route --eval-template [--json]"),
|
|
127
191
|
"routes": ("Usage:", "design-ai routes [--json]", "Equivalent to: design-ai route --list"),
|
|
128
192
|
"prompt": (
|
|
129
193
|
"Usage:",
|
|
130
|
-
"design-ai prompt <brief> [--route id] [--with-learning] [--learning-category kind] [--learning-limit N] [--json] [--out file] [--force]",
|
|
194
|
+
"design-ai prompt <brief> [--route id] [--with-learning] [--learning-category kind] [--learning-limit N] [--with-recall] [--recall-limit N] [--json] [--out file] [--force]",
|
|
131
195
|
"design-ai prompt --eval-template [--json] [--out file] [--force]",
|
|
132
196
|
"design-ai prompt --eval --from-file prompt-eval.json [--strict] [--json] [--out file] [--force]",
|
|
133
197
|
),
|
|
134
198
|
"pack": (
|
|
135
199
|
"Usage:",
|
|
136
|
-
"design-ai pack <brief> [--route id] [--with-learning] [--learning-category kind] [--learning-limit N] [--max-bytes N] [--json] [--out file] [--force]",
|
|
200
|
+
"design-ai pack <brief> [--route id] [--with-learning] [--learning-category kind] [--learning-limit N] [--with-recall] [--recall-limit N] [--max-bytes N] [--json] [--out file] [--force]",
|
|
137
201
|
"design-ai pack --eval-template [--json] [--out file] [--force]",
|
|
138
202
|
"design-ai pack --eval --from-file pack-eval.json [--strict] [--json] [--out file] [--force]",
|
|
139
203
|
),
|
|
@@ -144,6 +208,8 @@ EXPECTED_HELP_TOPIC_FRAGMENTS = {
|
|
|
144
208
|
"learn": (
|
|
145
209
|
"Usage:",
|
|
146
210
|
"design-ai learn [--list] [--category kind] [--query text] [--explain] [--limit N] [--json] [--out file] [--force]",
|
|
211
|
+
"design-ai learn --recall query [--limit N] [--category kind] [--json]",
|
|
212
|
+
"--recall",
|
|
147
213
|
"design-ai learn --init [--yes|--dry-run] [--json] [--out file] [--force]",
|
|
148
214
|
"design-ai learn --feedback text [--outcome keep|improve|avoid] [--category kind] [--json] [--out file] [--force]",
|
|
149
215
|
"design-ai learn --feedback --from-file notes.md [--outcome keep|improve|avoid] [--category kind] [--json] [--out file] [--force]",
|
|
@@ -307,7 +373,7 @@ EXPECTED_MAIN_HELP_FRAGMENTS = (
|
|
|
307
373
|
"pack <brief|--from-file file|--stdin|--eval-template|--eval>",
|
|
308
374
|
"check <artifact.md|--stdin|--examples>",
|
|
309
375
|
"examples [query]",
|
|
310
|
-
"learn [--init|--remember text|--feedback text|--list|--export|--query text|--explain|--backup|--redact|--verify|--diff|--restore|--restore-backups [--prune]|--import|--audit [--fix]|--curate|--stats|--usage|--signals [--strict]|--agent-backlog [--strict]|--propose-skills [--min-evidence N] [--review-file path] [--review-check|--apply-plan] [--strict]|--eval-template|--eval [--strict]|--forget id|--clear] [--json|--report|--patch|--review-template] [--out file]",
|
|
376
|
+
"learn [--init|--remember text|--feedback text|--list|--export|--query text|--explain|--recall query|--backup|--redact|--verify|--diff|--restore|--restore-backups [--prune]|--import|--audit [--fix]|--curate|--stats|--usage|--signals [--strict]|--agent-backlog [--strict]|--propose-skills [--min-evidence N] [--review-file path] [--review-check|--apply-plan] [--strict]|--eval-template|--eval [--strict]|--forget id|--clear] [--json|--report|--patch|--review-template] [--out file]",
|
|
311
377
|
"workspace [--root path] [--learning-file path] [--learning-usage path] [--learning-eval path] [--strict] [--json]",
|
|
312
378
|
"site <workspace.json|--stdin>",
|
|
313
379
|
"site --init",
|
|
@@ -327,7 +393,7 @@ EXPECTED_MAIN_HELP_FRAGMENTS = (
|
|
|
327
393
|
EXPECTED_VERSION_FRAGMENTS = (
|
|
328
394
|
"design-ai CLI:",
|
|
329
395
|
"Plugin / corpus:",
|
|
330
|
-
|
|
396
|
+
EXPECTED_RELEASE_VERSION,
|
|
331
397
|
"Source:",
|
|
332
398
|
)
|
|
333
399
|
EXPECTED_INSTALL_OUTPUT_FRAGMENTS = (
|
|
@@ -367,7 +433,7 @@ EXPECTED_DOCTOR_STRICT_OUTPUT_FRAGMENTS = (
|
|
|
367
433
|
"Target:",
|
|
368
434
|
"Prefix:",
|
|
369
435
|
"Source layout: complete",
|
|
370
|
-
"Version alignment:
|
|
436
|
+
f"Version alignment: {EXPECTED_RELEASE_VERSION}",
|
|
371
437
|
"Manifest paths: 41 referenced artifact(s) exist",
|
|
372
438
|
"Node runtime:",
|
|
373
439
|
"Python runtime:",
|
|
@@ -515,6 +581,39 @@ EXPECTED_ERROR_PREFIX = "\u2717"
|
|
|
515
581
|
EXPECTED_CORPUS_SEARCH_QUERY = "Pretendard"
|
|
516
582
|
EXPECTED_CORPUS_SEARCH_HIT = "knowledge/PRINCIPLES.md"
|
|
517
583
|
EXPECTED_CORPUS_SEARCH_PREVIEW = "Pretendard for Korean primary"
|
|
584
|
+
EXPECTED_RANKED_SEARCH_LIMIT = 3
|
|
585
|
+
EXPECTED_RANKED_SEARCH_NOT_BUILT_NOTICE = (
|
|
586
|
+
"no corpus index built yet; ranked results come from a live corpus scan (design-ai index --build)"
|
|
587
|
+
)
|
|
588
|
+
EXPECTED_INDEX_BUILD_COMMAND = "design-ai index --build"
|
|
589
|
+
EXPECTED_INDEX_FILE_BASENAMES = {
|
|
590
|
+
"corpus": "corpus-index.json",
|
|
591
|
+
"learning": "learning-index.json",
|
|
592
|
+
}
|
|
593
|
+
EXPECTED_EMBEDDING_INDEX_FILE_BASENAMES = {"embedding-index": "embedding-index.json"}
|
|
594
|
+
EXPECTED_INDEX_STATUS_SECTION_KEYS = [
|
|
595
|
+
"file",
|
|
596
|
+
"present",
|
|
597
|
+
"fresh",
|
|
598
|
+
"sourceMatch",
|
|
599
|
+
"generatedAt",
|
|
600
|
+
"documentCount",
|
|
601
|
+
"storedDigest",
|
|
602
|
+
"currentDigest",
|
|
603
|
+
"error",
|
|
604
|
+
]
|
|
605
|
+
EXPECTED_INDEX_STATUS_EMBEDDINGS_KEYS = [
|
|
606
|
+
"file",
|
|
607
|
+
"present",
|
|
608
|
+
"fresh",
|
|
609
|
+
"sourceMatch",
|
|
610
|
+
"generatedAt",
|
|
611
|
+
"documentCount",
|
|
612
|
+
"provider",
|
|
613
|
+
"storedDigest",
|
|
614
|
+
"currentDigest",
|
|
615
|
+
"error",
|
|
616
|
+
]
|
|
518
617
|
EXPECTED_CORPUS_SHOW_TARGET = "knowledge/PRINCIPLES.md:1"
|
|
519
618
|
EXPECTED_CORPUS_SHOW_REL_PATH = "knowledge/PRINCIPLES.md"
|
|
520
619
|
EXPECTED_CORPUS_SHOW_TEXT = "<!-- hand-written -->"
|
|
@@ -743,7 +842,8 @@ EXPECTED_CHECK_EXAMPLES_PAYLOAD_KEYS = [
|
|
|
743
842
|
"examples",
|
|
744
843
|
]
|
|
745
844
|
EXPECTED_CHECK_EXAMPLE_ENTRY_KEYS = ["example", "report"]
|
|
746
|
-
EXPECTED_WORKSPACE_PAYLOAD_KEYS = ["context", "git", "repository", "learning", "learningUsage", "learningEval", "learningRestoreBackups", "release", "nextActions"]
|
|
845
|
+
EXPECTED_WORKSPACE_PAYLOAD_KEYS = ["context", "git", "repository", "learning", "learningUsage", "learningEval", "learningRestoreBackups", "retrievalIndex", "release", "nextActions"]
|
|
846
|
+
EXPECTED_WORKSPACE_RETRIEVAL_INDEX_KEYS = ["indexDir", "corpus", "learning", "fresh", "status", "buildCommand"]
|
|
747
847
|
EXPECTED_WORKSPACE_CONTEXT_KEYS = ["cwd", "root", "sourceRoot", "packageName", "version"]
|
|
748
848
|
EXPECTED_WORKSPACE_GIT_KEYS = [
|
|
749
849
|
"isRepo",
|
|
@@ -1508,7 +1608,7 @@ def passing_list_catalog_output(kind: str = "skills") -> str:
|
|
|
1508
1608
|
"",
|
|
1509
1609
|
" design-ai catalog",
|
|
1510
1610
|
"",
|
|
1511
|
-
"Plugin: design-ai
|
|
1611
|
+
f"Plugin: design-ai v{EXPECTED_RELEASE_VERSION}",
|
|
1512
1612
|
"",
|
|
1513
1613
|
"",
|
|
1514
1614
|
f"{kind} ({len(items)})",
|
|
@@ -1540,7 +1640,7 @@ def passing_list_catalog_json(kind: str = "skills") -> str:
|
|
|
1540
1640
|
return json.dumps(
|
|
1541
1641
|
{
|
|
1542
1642
|
"name": "design-ai",
|
|
1543
|
-
"version":
|
|
1643
|
+
"version": EXPECTED_RELEASE_VERSION,
|
|
1544
1644
|
"kind": kind,
|
|
1545
1645
|
"sections": [
|
|
1546
1646
|
{
|
|
@@ -1871,6 +1971,105 @@ def passing_search_human_output() -> str:
|
|
|
1871
1971
|
])
|
|
1872
1972
|
|
|
1873
1973
|
|
|
1974
|
+
def passing_ranked_search_json(*, backend: str | None = None) -> str:
|
|
1975
|
+
payload = {
|
|
1976
|
+
"query": EXPECTED_CORPUS_SEARCH_QUERY,
|
|
1977
|
+
"ranked": True,
|
|
1978
|
+
"notice": EXPECTED_RANKED_SEARCH_NOT_BUILT_NOTICE,
|
|
1979
|
+
"hits": [
|
|
1980
|
+
{
|
|
1981
|
+
"relPath": EXPECTED_CORPUS_SEARCH_HIT,
|
|
1982
|
+
"file": f"/tmp/design-ai/{EXPECTED_CORPUS_SEARCH_HIT}",
|
|
1983
|
+
"score": 2.886971,
|
|
1984
|
+
"matchedTokens": [EXPECTED_CORPUS_SEARCH_QUERY.lower()],
|
|
1985
|
+
"preview": EXPECTED_CORPUS_SEARCH_PREVIEW,
|
|
1986
|
+
}
|
|
1987
|
+
],
|
|
1988
|
+
}
|
|
1989
|
+
if backend is not None:
|
|
1990
|
+
payload["backend"] = backend
|
|
1991
|
+
return json.dumps(payload)
|
|
1992
|
+
|
|
1993
|
+
|
|
1994
|
+
def passing_index_build_json(index_dir: str = "/tmp/design-ai-index") -> str:
|
|
1995
|
+
return json.dumps({
|
|
1996
|
+
"action": "build",
|
|
1997
|
+
"indexDir": index_dir,
|
|
1998
|
+
"corpus": {
|
|
1999
|
+
"file": f"{index_dir}/{EXPECTED_INDEX_FILE_BASENAMES['corpus']}",
|
|
2000
|
+
"documentCount": 460,
|
|
2001
|
+
"digest": f"sha256:{'0' * 64}",
|
|
2002
|
+
},
|
|
2003
|
+
"learning": {
|
|
2004
|
+
"file": f"{index_dir}/{EXPECTED_INDEX_FILE_BASENAMES['learning']}",
|
|
2005
|
+
"documentCount": 3,
|
|
2006
|
+
"digest": f"sha256:{'1' * 64}",
|
|
2007
|
+
},
|
|
2008
|
+
})
|
|
2009
|
+
|
|
2010
|
+
|
|
2011
|
+
def passing_index_status_section(index_dir: str, label: str, digest: str) -> dict:
|
|
2012
|
+
return {
|
|
2013
|
+
"file": f"{index_dir}/{EXPECTED_INDEX_FILE_BASENAMES[label]}",
|
|
2014
|
+
"present": True,
|
|
2015
|
+
"fresh": True,
|
|
2016
|
+
"sourceMatch": True,
|
|
2017
|
+
"generatedAt": "2026-05-22T00:00:03.000Z",
|
|
2018
|
+
"documentCount": 460 if label == "corpus" else 3,
|
|
2019
|
+
"storedDigest": digest,
|
|
2020
|
+
"currentDigest": digest,
|
|
2021
|
+
"error": "",
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
|
|
2025
|
+
def passing_index_status_embeddings_section(index_dir: str) -> dict:
|
|
2026
|
+
return {
|
|
2027
|
+
"file": f"{index_dir}/{EXPECTED_EMBEDDING_INDEX_FILE_BASENAMES['embedding-index']}",
|
|
2028
|
+
"present": True,
|
|
2029
|
+
"fresh": True,
|
|
2030
|
+
"sourceMatch": True,
|
|
2031
|
+
"generatedAt": "2026-05-22T00:00:03.000Z",
|
|
2032
|
+
"documentCount": 463,
|
|
2033
|
+
"provider": {"command": "./bin/local-embed", "modelLabel": "stub", "dimensions": 3},
|
|
2034
|
+
"storedDigest": f"sha256:{'0' * 64}",
|
|
2035
|
+
"currentDigest": f"sha256:{'0' * 64}",
|
|
2036
|
+
"error": "",
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
|
|
2040
|
+
def passing_index_status_json(index_dir: str = "/tmp/design-ai-index", *, with_embeddings: bool = False) -> str:
|
|
2041
|
+
return json.dumps({
|
|
2042
|
+
"action": "status",
|
|
2043
|
+
"indexDir": index_dir,
|
|
2044
|
+
"corpus": passing_index_status_section(index_dir, "corpus", f"sha256:{'0' * 64}"),
|
|
2045
|
+
"learning": passing_index_status_section(index_dir, "learning", f"sha256:{'1' * 64}"),
|
|
2046
|
+
"embeddings": passing_index_status_embeddings_section(index_dir) if with_embeddings else None,
|
|
2047
|
+
"fresh": True,
|
|
2048
|
+
"buildCommand": EXPECTED_INDEX_BUILD_COMMAND,
|
|
2049
|
+
})
|
|
2050
|
+
|
|
2051
|
+
|
|
2052
|
+
def passing_index_verify_json(index_dir: str = "/tmp/design-ai-index") -> str:
|
|
2053
|
+
return json.dumps({
|
|
2054
|
+
"action": "verify",
|
|
2055
|
+
"ok": True,
|
|
2056
|
+
"checks": [
|
|
2057
|
+
{
|
|
2058
|
+
"name": "corpus",
|
|
2059
|
+
"file": f"{index_dir}/{EXPECTED_INDEX_FILE_BASENAMES['corpus']}",
|
|
2060
|
+
"matches": True,
|
|
2061
|
+
"reason": "",
|
|
2062
|
+
},
|
|
2063
|
+
{
|
|
2064
|
+
"name": "learning",
|
|
2065
|
+
"file": f"{index_dir}/{EXPECTED_INDEX_FILE_BASENAMES['learning']}",
|
|
2066
|
+
"matches": True,
|
|
2067
|
+
"reason": "",
|
|
2068
|
+
},
|
|
2069
|
+
],
|
|
2070
|
+
})
|
|
2071
|
+
|
|
2072
|
+
|
|
1874
2073
|
def passing_show_json() -> str:
|
|
1875
2074
|
return json.dumps({
|
|
1876
2075
|
"file": "/tmp/design-ai/knowledge/PRINCIPLES.md",
|
|
@@ -1961,7 +2160,7 @@ def passing_examples_human_output() -> str:
|
|
|
1961
2160
|
def passing_route_json() -> str:
|
|
1962
2161
|
return json.dumps({
|
|
1963
2162
|
"brief": EXPECTED_ROUTE_BRIEF,
|
|
1964
|
-
"version":
|
|
2163
|
+
"version": EXPECTED_RELEASE_VERSION,
|
|
1965
2164
|
"routes": [
|
|
1966
2165
|
{
|
|
1967
2166
|
"id": EXPECTED_ROUTE_ID,
|
|
@@ -2035,7 +2234,7 @@ def passing_route_explain_human_output() -> str:
|
|
|
2035
2234
|
f" {EXPECTED_ROUTE_BRIEF}",
|
|
2036
2235
|
"",
|
|
2037
2236
|
"Source: /tmp/design-ai",
|
|
2038
|
-
"Corpus version:
|
|
2237
|
+
f"Corpus version: {EXPECTED_RELEASE_VERSION}",
|
|
2039
2238
|
"",
|
|
2040
2239
|
f"1. {EXPECTED_ROUTE_LABEL} (high, score {len(EXPECTED_ROUTE_MATCHED_KEYWORDS)})",
|
|
2041
2240
|
f" id: {EXPECTED_ROUTE_ID}",
|
|
@@ -2124,7 +2323,7 @@ def passing_route_catalog_json() -> str:
|
|
|
2124
2323
|
routes.append(route)
|
|
2125
2324
|
|
|
2126
2325
|
return json.dumps({
|
|
2127
|
-
"version":
|
|
2326
|
+
"version": EXPECTED_RELEASE_VERSION,
|
|
2128
2327
|
"routes": routes,
|
|
2129
2328
|
})
|
|
2130
2329
|
|
|
@@ -2132,7 +2331,7 @@ def passing_route_catalog_json() -> str:
|
|
|
2132
2331
|
def passing_prompt_payload() -> dict:
|
|
2133
2332
|
return {
|
|
2134
2333
|
"brief": EXPECTED_ROUTE_BRIEF,
|
|
2135
|
-
"version":
|
|
2334
|
+
"version": EXPECTED_RELEASE_VERSION,
|
|
2136
2335
|
"route": {
|
|
2137
2336
|
"id": EXPECTED_ROUTE_ID,
|
|
2138
2337
|
"label": EXPECTED_ROUTE_LABEL,
|
|
@@ -2237,7 +2436,7 @@ def passing_prompt_markdown_output() -> str:
|
|
|
2237
2436
|
f" {EXPECTED_ROUTE_BRIEF}",
|
|
2238
2437
|
"",
|
|
2239
2438
|
"Source: /tmp/design",
|
|
2240
|
-
"Corpus version:
|
|
2439
|
+
f"Corpus version: {EXPECTED_RELEASE_VERSION}",
|
|
2241
2440
|
"",
|
|
2242
2441
|
"# design-ai task prompt",
|
|
2243
2442
|
f"Task: {EXPECTED_ROUTE_BRIEF}",
|
|
@@ -2263,7 +2462,7 @@ def passing_prompt_markdown_output() -> str:
|
|
|
2263
2462
|
def passing_pack_json() -> str:
|
|
2264
2463
|
return json.dumps({
|
|
2265
2464
|
"brief": EXPECTED_ROUTE_BRIEF,
|
|
2266
|
-
"version":
|
|
2465
|
+
"version": EXPECTED_RELEASE_VERSION,
|
|
2267
2466
|
"maxBytes": EXPECTED_PACK_MAX_BYTES,
|
|
2268
2467
|
"usedBytes": EXPECTED_PACK_MAX_BYTES,
|
|
2269
2468
|
"summary": {
|
|
@@ -2311,7 +2510,7 @@ def passing_pack_markdown_output() -> str:
|
|
|
2311
2510
|
f" {EXPECTED_ROUTE_BRIEF}",
|
|
2312
2511
|
"",
|
|
2313
2512
|
"Source: /tmp/design",
|
|
2314
|
-
"Corpus version:
|
|
2513
|
+
f"Corpus version: {EXPECTED_RELEASE_VERSION}",
|
|
2315
2514
|
f"Context: partial, {EXPECTED_PACK_MAX_BYTES}/{EXPECTED_PACK_MAX_BYTES} bytes, 2 warnings",
|
|
2316
2515
|
"",
|
|
2317
2516
|
"# design-ai prompt pack",
|
|
@@ -2606,6 +2805,321 @@ def assert_search_human_output(raw: str, *, context: str, cmd: list[str]) -> Non
|
|
|
2606
2805
|
)
|
|
2607
2806
|
|
|
2608
2807
|
|
|
2808
|
+
def is_ranked_search_positive_score(value: object) -> bool:
|
|
2809
|
+
return type(value) in (int, float) and value > 0
|
|
2810
|
+
|
|
2811
|
+
|
|
2812
|
+
def is_index_sha256_digest(value: object) -> bool:
|
|
2813
|
+
if not isinstance(value, str) or not value.startswith("sha256:"):
|
|
2814
|
+
return False
|
|
2815
|
+
digest = value[len("sha256:"):]
|
|
2816
|
+
return len(digest) == 64 and all(char in "0123456789abcdef" for char in digest)
|
|
2817
|
+
|
|
2818
|
+
|
|
2819
|
+
def assert_index_file_path(
|
|
2820
|
+
value: object,
|
|
2821
|
+
*,
|
|
2822
|
+
index_dir: str,
|
|
2823
|
+
label: str,
|
|
2824
|
+
context: str,
|
|
2825
|
+
command_label: str,
|
|
2826
|
+
) -> None:
|
|
2827
|
+
expected = str(Path(index_dir) / EXPECTED_INDEX_FILE_BASENAMES[label])
|
|
2828
|
+
if value != expected:
|
|
2829
|
+
raise SystemExit(f"{command_label} after {context} {label} file differs from expected index path")
|
|
2830
|
+
|
|
2831
|
+
|
|
2832
|
+
def assert_ranked_search_json(
|
|
2833
|
+
raw: str,
|
|
2834
|
+
*,
|
|
2835
|
+
context: str,
|
|
2836
|
+
cmd: list[str],
|
|
2837
|
+
expected_notice: str | None = None,
|
|
2838
|
+
expected_backend: str | None = None,
|
|
2839
|
+
) -> None:
|
|
2840
|
+
assert_no_ansi(raw, cmd)
|
|
2841
|
+
try:
|
|
2842
|
+
payload = json.loads(raw)
|
|
2843
|
+
except json.JSONDecodeError as error:
|
|
2844
|
+
raise SystemExit(f"failed to parse ranked search JSON after {context}") from error
|
|
2845
|
+
|
|
2846
|
+
expected_keys = ["query", "ranked", "notice", "hits"]
|
|
2847
|
+
if expected_backend is not None:
|
|
2848
|
+
expected_keys.append("backend")
|
|
2849
|
+
payload = assert_corpus_json_keys(
|
|
2850
|
+
payload,
|
|
2851
|
+
expected_keys,
|
|
2852
|
+
label="top-level",
|
|
2853
|
+
context=context,
|
|
2854
|
+
command_label="ranked search JSON",
|
|
2855
|
+
)
|
|
2856
|
+
|
|
2857
|
+
if payload.get("query") != EXPECTED_CORPUS_SEARCH_QUERY:
|
|
2858
|
+
raise SystemExit(f"ranked search JSON after {context} query differs from expected query")
|
|
2859
|
+
if payload.get("ranked") is not True:
|
|
2860
|
+
raise SystemExit(f"ranked search JSON after {context} ranked flag is not true")
|
|
2861
|
+
notice = payload.get("notice")
|
|
2862
|
+
if not isinstance(notice, str):
|
|
2863
|
+
raise SystemExit(f"ranked search JSON after {context} notice is not a string")
|
|
2864
|
+
if expected_notice is not None and notice != expected_notice:
|
|
2865
|
+
raise SystemExit(f"ranked search JSON after {context} notice differs from expected notice")
|
|
2866
|
+
if expected_backend is not None and payload.get("backend") != expected_backend:
|
|
2867
|
+
raise SystemExit(f"ranked search JSON after {context} backend differs from expected backend")
|
|
2868
|
+
|
|
2869
|
+
hits = payload.get("hits")
|
|
2870
|
+
if not isinstance(hits, list):
|
|
2871
|
+
raise SystemExit(f"ranked search JSON after {context} hits is not a list")
|
|
2872
|
+
if not hits:
|
|
2873
|
+
raise SystemExit(f"ranked search JSON after {context} does not contain any hits")
|
|
2874
|
+
if len(hits) > EXPECTED_RANKED_SEARCH_LIMIT:
|
|
2875
|
+
raise SystemExit(f"ranked search JSON after {context} hit count exceeds the expected limit")
|
|
2876
|
+
|
|
2877
|
+
query_token = EXPECTED_CORPUS_SEARCH_QUERY.lower()
|
|
2878
|
+
query_token_matched = False
|
|
2879
|
+
for hit in hits:
|
|
2880
|
+
hit = assert_corpus_json_keys(
|
|
2881
|
+
hit,
|
|
2882
|
+
["relPath", "file", "score", "matchedTokens", "preview"],
|
|
2883
|
+
label="hit",
|
|
2884
|
+
context=context,
|
|
2885
|
+
command_label="ranked search JSON",
|
|
2886
|
+
)
|
|
2887
|
+
rel_path = hit.get("relPath")
|
|
2888
|
+
if not isinstance(rel_path, str) or not rel_path.startswith("knowledge/"):
|
|
2889
|
+
raise SystemExit(f"ranked search JSON after {context} hit relPath is outside the knowledge dir")
|
|
2890
|
+
assert_corpus_file_path(
|
|
2891
|
+
hit.get("file"),
|
|
2892
|
+
rel_path,
|
|
2893
|
+
label="hit",
|
|
2894
|
+
context=context,
|
|
2895
|
+
command_label="ranked search JSON",
|
|
2896
|
+
)
|
|
2897
|
+
if not is_ranked_search_positive_score(hit.get("score")):
|
|
2898
|
+
raise SystemExit(f"ranked search JSON after {context} hit score is not a positive number")
|
|
2899
|
+
matched_tokens = hit.get("matchedTokens")
|
|
2900
|
+
if not isinstance(matched_tokens, list) or not matched_tokens:
|
|
2901
|
+
raise SystemExit(f"ranked search JSON after {context} hit matchedTokens is empty")
|
|
2902
|
+
if not all(isinstance(token, str) and token for token in matched_tokens):
|
|
2903
|
+
raise SystemExit(f"ranked search JSON after {context} hit matchedTokens contains an invalid token")
|
|
2904
|
+
if not isinstance(hit.get("preview"), str) or not hit.get("preview"):
|
|
2905
|
+
raise SystemExit(f"ranked search JSON after {context} hit preview is empty")
|
|
2906
|
+
if query_token in matched_tokens:
|
|
2907
|
+
query_token_matched = True
|
|
2908
|
+
|
|
2909
|
+
if not query_token_matched:
|
|
2910
|
+
raise SystemExit(f"ranked search JSON after {context} is missing a hit matching the query token")
|
|
2911
|
+
|
|
2912
|
+
|
|
2913
|
+
def assert_ranked_search_determinism(
|
|
2914
|
+
first_raw: str,
|
|
2915
|
+
second_raw: str,
|
|
2916
|
+
*,
|
|
2917
|
+
context: str,
|
|
2918
|
+
cmd: list[str],
|
|
2919
|
+
) -> None:
|
|
2920
|
+
assert_no_ansi(first_raw, cmd)
|
|
2921
|
+
assert_no_ansi(second_raw, cmd)
|
|
2922
|
+
if first_raw != second_raw:
|
|
2923
|
+
raise SystemExit(f"ranked search JSON after {context} differs between identical runs")
|
|
2924
|
+
|
|
2925
|
+
|
|
2926
|
+
def assert_index_build_json(raw: str, *, index_dir: str, context: str, cmd: list[str]) -> None:
|
|
2927
|
+
assert_no_ansi(raw, cmd)
|
|
2928
|
+
try:
|
|
2929
|
+
payload = json.loads(raw)
|
|
2930
|
+
except json.JSONDecodeError as error:
|
|
2931
|
+
raise SystemExit(f"failed to parse index build JSON after {context}") from error
|
|
2932
|
+
|
|
2933
|
+
payload = assert_corpus_json_keys(
|
|
2934
|
+
payload,
|
|
2935
|
+
["action", "indexDir", "corpus", "learning"],
|
|
2936
|
+
label="top-level",
|
|
2937
|
+
context=context,
|
|
2938
|
+
command_label="index build JSON",
|
|
2939
|
+
)
|
|
2940
|
+
|
|
2941
|
+
if payload.get("action") != "build":
|
|
2942
|
+
raise SystemExit(f"index build JSON after {context} action differs from build")
|
|
2943
|
+
if payload.get("indexDir") != str(index_dir):
|
|
2944
|
+
raise SystemExit(f"index build JSON after {context} indexDir differs from expected directory")
|
|
2945
|
+
|
|
2946
|
+
for label in EXPECTED_INDEX_FILE_BASENAMES:
|
|
2947
|
+
section = assert_corpus_json_keys(
|
|
2948
|
+
payload.get(label),
|
|
2949
|
+
["file", "documentCount", "digest"],
|
|
2950
|
+
label=label,
|
|
2951
|
+
context=context,
|
|
2952
|
+
command_label="index build JSON",
|
|
2953
|
+
)
|
|
2954
|
+
assert_index_file_path(
|
|
2955
|
+
section.get("file"),
|
|
2956
|
+
index_dir=index_dir,
|
|
2957
|
+
label=label,
|
|
2958
|
+
context=context,
|
|
2959
|
+
command_label="index build JSON",
|
|
2960
|
+
)
|
|
2961
|
+
if not is_corpus_json_positive_int(section.get("documentCount")):
|
|
2962
|
+
raise SystemExit(f"index build JSON after {context} {label} documentCount is not a positive integer")
|
|
2963
|
+
if not is_index_sha256_digest(section.get("digest")):
|
|
2964
|
+
raise SystemExit(f"index build JSON after {context} {label} digest is not a sha256 digest")
|
|
2965
|
+
|
|
2966
|
+
|
|
2967
|
+
def assert_index_status_json(
|
|
2968
|
+
raw: str,
|
|
2969
|
+
*,
|
|
2970
|
+
index_dir: str,
|
|
2971
|
+
context: str,
|
|
2972
|
+
cmd: list[str],
|
|
2973
|
+
expect_embeddings: bool = False,
|
|
2974
|
+
) -> None:
|
|
2975
|
+
assert_no_ansi(raw, cmd)
|
|
2976
|
+
try:
|
|
2977
|
+
payload = json.loads(raw)
|
|
2978
|
+
except json.JSONDecodeError as error:
|
|
2979
|
+
raise SystemExit(f"failed to parse index status JSON after {context}") from error
|
|
2980
|
+
|
|
2981
|
+
payload = assert_corpus_json_keys(
|
|
2982
|
+
payload,
|
|
2983
|
+
["action", "indexDir", "corpus", "learning", "embeddings", "fresh", "buildCommand"],
|
|
2984
|
+
label="top-level",
|
|
2985
|
+
context=context,
|
|
2986
|
+
command_label="index status JSON",
|
|
2987
|
+
)
|
|
2988
|
+
|
|
2989
|
+
if payload.get("action") != "status":
|
|
2990
|
+
raise SystemExit(f"index status JSON after {context} action differs from status")
|
|
2991
|
+
if payload.get("indexDir") != str(index_dir):
|
|
2992
|
+
raise SystemExit(f"index status JSON after {context} indexDir differs from expected directory")
|
|
2993
|
+
if payload.get("fresh") is not True:
|
|
2994
|
+
raise SystemExit(f"index status JSON after {context} is not fresh")
|
|
2995
|
+
if payload.get("buildCommand") != EXPECTED_INDEX_BUILD_COMMAND:
|
|
2996
|
+
raise SystemExit(f"index status JSON after {context} buildCommand differs from expected command")
|
|
2997
|
+
|
|
2998
|
+
for label in EXPECTED_INDEX_FILE_BASENAMES:
|
|
2999
|
+
section = assert_corpus_json_keys(
|
|
3000
|
+
payload.get(label),
|
|
3001
|
+
EXPECTED_INDEX_STATUS_SECTION_KEYS,
|
|
3002
|
+
label=label,
|
|
3003
|
+
context=context,
|
|
3004
|
+
command_label="index status JSON",
|
|
3005
|
+
)
|
|
3006
|
+
assert_index_file_path(
|
|
3007
|
+
section.get("file"),
|
|
3008
|
+
index_dir=index_dir,
|
|
3009
|
+
label=label,
|
|
3010
|
+
context=context,
|
|
3011
|
+
command_label="index status JSON",
|
|
3012
|
+
)
|
|
3013
|
+
if section.get("present") is not True:
|
|
3014
|
+
raise SystemExit(f"index status JSON after {context} {label} index is not present")
|
|
3015
|
+
if section.get("fresh") is not True:
|
|
3016
|
+
raise SystemExit(f"index status JSON after {context} {label} index is not fresh")
|
|
3017
|
+
generated_at = section.get("generatedAt")
|
|
3018
|
+
if not isinstance(generated_at, str) or not generated_at:
|
|
3019
|
+
raise SystemExit(f"index status JSON after {context} {label} generatedAt is missing")
|
|
3020
|
+
if not is_corpus_json_positive_int(section.get("documentCount")):
|
|
3021
|
+
raise SystemExit(f"index status JSON after {context} {label} documentCount is not a positive integer")
|
|
3022
|
+
if not is_index_sha256_digest(section.get("storedDigest")):
|
|
3023
|
+
raise SystemExit(f"index status JSON after {context} {label} storedDigest is not a sha256 digest")
|
|
3024
|
+
if section.get("storedDigest") != section.get("currentDigest"):
|
|
3025
|
+
raise SystemExit(f"index status JSON after {context} {label} stored digest differs from current digest")
|
|
3026
|
+
if section.get("error") != "":
|
|
3027
|
+
raise SystemExit(f"index status JSON after {context} {label} error is not empty")
|
|
3028
|
+
|
|
3029
|
+
embeddings = payload.get("embeddings")
|
|
3030
|
+
if not expect_embeddings:
|
|
3031
|
+
if embeddings is not None:
|
|
3032
|
+
raise SystemExit(f"index status JSON after {context} embeddings is not null when embeddings were not built")
|
|
3033
|
+
return
|
|
3034
|
+
|
|
3035
|
+
embeddings = assert_corpus_json_keys(
|
|
3036
|
+
embeddings,
|
|
3037
|
+
EXPECTED_INDEX_STATUS_EMBEDDINGS_KEYS,
|
|
3038
|
+
label="embeddings",
|
|
3039
|
+
context=context,
|
|
3040
|
+
command_label="index status JSON",
|
|
3041
|
+
)
|
|
3042
|
+
expected_embeddings_file = str(Path(index_dir) / EXPECTED_EMBEDDING_INDEX_FILE_BASENAMES["embedding-index"])
|
|
3043
|
+
if embeddings.get("file") != expected_embeddings_file:
|
|
3044
|
+
raise SystemExit(f"index status JSON after {context} embeddings file differs from expected index path")
|
|
3045
|
+
if embeddings.get("present") is not True:
|
|
3046
|
+
raise SystemExit(f"index status JSON after {context} embeddings index is not present")
|
|
3047
|
+
if embeddings.get("fresh") is not True:
|
|
3048
|
+
raise SystemExit(f"index status JSON after {context} embeddings index is not fresh")
|
|
3049
|
+
if not is_corpus_json_positive_int(embeddings.get("documentCount")):
|
|
3050
|
+
raise SystemExit(f"index status JSON after {context} embeddings documentCount is not a positive integer")
|
|
3051
|
+
provider = embeddings.get("provider")
|
|
3052
|
+
if not isinstance(provider, dict) or not provider.get("command"):
|
|
3053
|
+
raise SystemExit(f"index status JSON after {context} embeddings provider is missing a command")
|
|
3054
|
+
if embeddings.get("error") != "":
|
|
3055
|
+
raise SystemExit(f"index status JSON after {context} embeddings error is not empty")
|
|
3056
|
+
|
|
3057
|
+
|
|
3058
|
+
def assert_index_verify_json(
|
|
3059
|
+
raw: str,
|
|
3060
|
+
*,
|
|
3061
|
+
index_dir: str,
|
|
3062
|
+
context: str,
|
|
3063
|
+
cmd: list[str],
|
|
3064
|
+
expect_embeddings_check: bool = False,
|
|
3065
|
+
) -> None:
|
|
3066
|
+
assert_no_ansi(raw, cmd)
|
|
3067
|
+
try:
|
|
3068
|
+
payload = json.loads(raw)
|
|
3069
|
+
except json.JSONDecodeError as error:
|
|
3070
|
+
raise SystemExit(f"failed to parse index verify JSON after {context}") from error
|
|
3071
|
+
|
|
3072
|
+
payload = assert_corpus_json_keys(
|
|
3073
|
+
payload,
|
|
3074
|
+
["action", "ok", "checks"],
|
|
3075
|
+
label="top-level",
|
|
3076
|
+
context=context,
|
|
3077
|
+
command_label="index verify JSON",
|
|
3078
|
+
)
|
|
3079
|
+
|
|
3080
|
+
if payload.get("action") != "verify":
|
|
3081
|
+
raise SystemExit(f"index verify JSON after {context} action differs from verify")
|
|
3082
|
+
if payload.get("ok") is not True:
|
|
3083
|
+
raise SystemExit(f"index verify JSON after {context} ok is not true")
|
|
3084
|
+
|
|
3085
|
+
checks = payload.get("checks")
|
|
3086
|
+
if not isinstance(checks, list):
|
|
3087
|
+
raise SystemExit(f"index verify JSON after {context} checks is not a list")
|
|
3088
|
+
expected_names = list(EXPECTED_INDEX_FILE_BASENAMES) + (["embeddings"] if expect_embeddings_check else [])
|
|
3089
|
+
if [check.get("name") for check in checks if isinstance(check, dict)] != expected_names:
|
|
3090
|
+
raise SystemExit(f"index verify JSON after {context} check names changed")
|
|
3091
|
+
|
|
3092
|
+
for check in checks:
|
|
3093
|
+
name = check.get("name") if isinstance(check, dict) else None
|
|
3094
|
+
if name == "embeddings":
|
|
3095
|
+
if check.get("matches") is not True:
|
|
3096
|
+
raise SystemExit(f"index verify JSON after {context} embeddings check does not match")
|
|
3097
|
+
if not isinstance(check.get("file"), str) or not check.get("file"):
|
|
3098
|
+
raise SystemExit(f"index verify JSON after {context} embeddings check file is missing")
|
|
3099
|
+
if not isinstance(check.get("reason"), str):
|
|
3100
|
+
raise SystemExit(f"index verify JSON after {context} embeddings check reason is not a string")
|
|
3101
|
+
continue
|
|
3102
|
+
|
|
3103
|
+
check = assert_corpus_json_keys(
|
|
3104
|
+
check,
|
|
3105
|
+
["name", "file", "matches", "reason"],
|
|
3106
|
+
label="check",
|
|
3107
|
+
context=context,
|
|
3108
|
+
command_label="index verify JSON",
|
|
3109
|
+
)
|
|
3110
|
+
assert_index_file_path(
|
|
3111
|
+
check.get("file"),
|
|
3112
|
+
index_dir=index_dir,
|
|
3113
|
+
label=name,
|
|
3114
|
+
context=context,
|
|
3115
|
+
command_label="index verify JSON",
|
|
3116
|
+
)
|
|
3117
|
+
if check.get("matches") is not True:
|
|
3118
|
+
raise SystemExit(f"index verify JSON after {context} {name} check does not match")
|
|
3119
|
+
if check.get("reason") != "":
|
|
3120
|
+
raise SystemExit(f"index verify JSON after {context} {name} check reason is not empty")
|
|
3121
|
+
|
|
3122
|
+
|
|
2609
3123
|
def assert_show_json_line(raw: str, *, context: str, cmd: list[str]) -> None:
|
|
2610
3124
|
assert_no_ansi(raw, cmd)
|
|
2611
3125
|
try:
|
|
@@ -4152,18 +4666,21 @@ def passing_main_help_output() -> str:
|
|
|
4152
4666
|
" design-ai help [command|--json]",
|
|
4153
4667
|
"",
|
|
4154
4668
|
" install Symlink design-ai into Claude Code (~/.claude)",
|
|
4155
|
-
" search <query> [--dir kind] [--limit N] [--
|
|
4669
|
+
" search <query> [--dir kind] [--limit N] [--ranked] [--embeddings [--provider \"cmd args\"]] [--json]",
|
|
4670
|
+
" Search the local markdown corpus",
|
|
4671
|
+
" index [--build|--status|--verify] [--json] [--embeddings [--provider \"cmd args\"]]",
|
|
4672
|
+
" Build, inspect, and verify the local retrieval index",
|
|
4156
4673
|
" show <file[:line]> [--lines N:M] [--context N] [--json] Print a corpus file or line range",
|
|
4157
4674
|
" route <brief|--from-file file|--stdin|--list|--eval-template|--eval> [--limit N]",
|
|
4158
4675
|
" Recommend commands, skills, knowledge, and route eval checkpoints",
|
|
4159
|
-
" prompt <brief|--from-file file|--stdin|--eval-template|--eval> [--route id] [--with-learning] [--learning-category kind] [--learning-limit N] [--out file]",
|
|
4676
|
+
" prompt <brief|--from-file file|--stdin|--eval-template|--eval> [--route id] [--with-learning] [--learning-category kind] [--learning-limit N] [--with-recall] [--recall-limit N] [--out file]",
|
|
4160
4677
|
" Generate a ready-to-use agent prompt and prompt-plan eval checkpoints",
|
|
4161
|
-
" pack <brief|--from-file file|--stdin|--eval-template|--eval> [--route id] [--with-learning] [--learning-category kind] [--learning-limit N] [--max-bytes N]",
|
|
4678
|
+
" pack <brief|--from-file file|--stdin|--eval-template|--eval> [--route id] [--with-learning] [--learning-category kind] [--learning-limit N] [--with-recall] [--recall-limit N] [--max-bytes N]",
|
|
4162
4679
|
" Generate prompt plus bounded context and prompt-pack eval checkpoints",
|
|
4163
4680
|
" check <artifact.md|--stdin|--examples> [--route id|--all-routes] [--learn]",
|
|
4164
4681
|
" Check generated Markdown artifact quality; add --issues-only or --learn",
|
|
4165
4682
|
" examples [query] [--route id] [--limit N] [--json] Find worked examples for a route or query",
|
|
4166
|
-
" learn [--init|--remember text|--feedback text|--list|--export|--query text|--explain|--backup|--redact|--verify|--diff|--restore|--restore-backups [--prune]|--import|--audit [--fix]|--curate|--stats|--usage|--signals [--strict]|--agent-backlog [--strict]|--propose-skills [--min-evidence N] [--review-file path] [--review-check|--apply-plan] [--strict]|--eval-template|--eval [--strict]|--forget id|--clear] [--json|--report|--patch|--review-template] [--out file]",
|
|
4683
|
+
" learn [--init|--remember text|--feedback text|--list|--export|--query text|--explain|--recall query|--backup|--redact|--verify|--diff|--restore|--restore-backups [--prune]|--import|--audit [--fix]|--curate|--stats|--usage|--signals [--strict]|--agent-backlog [--strict]|--propose-skills [--min-evidence N] [--review-file path] [--review-check|--apply-plan] [--strict]|--eval-template|--eval [--strict]|--forget id|--clear] [--json|--report|--patch|--review-template] [--out file]",
|
|
4167
4684
|
" Manage local learning preferences, usage reports, signal registry, agent backlog, skill proposals, and eval checkpoints for prompt personalization",
|
|
4168
4685
|
" workspace [--root path] [--learning-file path] [--learning-usage path] [--learning-eval path] [--strict] [--json]",
|
|
4169
4686
|
" Show read-only local dogfood readiness: git, repository, learning usage, eval checkpoints, and release scripts",
|
|
@@ -4181,8 +4698,8 @@ def passing_main_help_output() -> str:
|
|
|
4181
4698
|
|
|
4182
4699
|
def passing_version_output() -> str:
|
|
4183
4700
|
return "\n".join([
|
|
4184
|
-
"design-ai CLI:
|
|
4185
|
-
"Plugin / corpus:
|
|
4701
|
+
f"design-ai CLI: {EXPECTED_RELEASE_VERSION}",
|
|
4702
|
+
f"Plugin / corpus: {EXPECTED_RELEASE_VERSION}",
|
|
4186
4703
|
"Source: /tmp/design-ai",
|
|
4187
4704
|
"",
|
|
4188
4705
|
])
|
|
@@ -4195,8 +4712,8 @@ def passing_version_json() -> str:
|
|
|
4195
4712
|
"sourceRoot": "/tmp/design-ai",
|
|
4196
4713
|
},
|
|
4197
4714
|
"versions": {
|
|
4198
|
-
"cli":
|
|
4199
|
-
"plugin":
|
|
4715
|
+
"cli": EXPECTED_RELEASE_VERSION,
|
|
4716
|
+
"plugin": EXPECTED_RELEASE_VERSION,
|
|
4200
4717
|
"aligned": True,
|
|
4201
4718
|
},
|
|
4202
4719
|
},
|
|
@@ -4213,7 +4730,7 @@ def passing_workspace_json() -> str:
|
|
|
4213
4730
|
"root": "/tmp/project",
|
|
4214
4731
|
"sourceRoot": "/tmp/design-ai",
|
|
4215
4732
|
"packageName": "@design-ai/cli",
|
|
4216
|
-
"version":
|
|
4733
|
+
"version": EXPECTED_RELEASE_VERSION,
|
|
4217
4734
|
},
|
|
4218
4735
|
"git": {
|
|
4219
4736
|
"isRepo": False,
|
|
@@ -4265,9 +4782,10 @@ def passing_workspace_json() -> str:
|
|
|
4265
4782
|
"learningUsage": None,
|
|
4266
4783
|
"learningEval": None,
|
|
4267
4784
|
"learningRestoreBackups": None,
|
|
4785
|
+
"retrievalIndex": None,
|
|
4268
4786
|
"release": {
|
|
4269
4787
|
"packageName": "@design-ai/cli",
|
|
4270
|
-
"version":
|
|
4788
|
+
"version": EXPECTED_RELEASE_VERSION,
|
|
4271
4789
|
"scripts": {
|
|
4272
4790
|
"test": "node --test cli/lib/*.test.mjs",
|
|
4273
4791
|
"audit:strict": "python3 -B tools/audit/run-all.py --strict",
|
|
@@ -5568,7 +6086,7 @@ def passing_doctor_strict_output() -> str:
|
|
|
5568
6086
|
"ℹ Prefix: smoke-design-",
|
|
5569
6087
|
"",
|
|
5570
6088
|
"✓ Source layout: complete at /tmp/design-ai",
|
|
5571
|
-
"✓ Version alignment:
|
|
6089
|
+
f"✓ Version alignment: {EXPECTED_RELEASE_VERSION}",
|
|
5572
6090
|
"✓ Manifest paths: 41 referenced artifact(s) exist",
|
|
5573
6091
|
"✓ Node runtime: v24.13.1",
|
|
5574
6092
|
"✓ Python runtime: Python 3.12.12",
|
|
@@ -5593,7 +6111,7 @@ def passing_install_output() -> str:
|
|
|
5593
6111
|
return "\n".join([
|
|
5594
6112
|
"",
|
|
5595
6113
|
" design-ai installer",
|
|
5596
|
-
"
|
|
6114
|
+
f" v{EXPECTED_RELEASE_VERSION}",
|
|
5597
6115
|
"",
|
|
5598
6116
|
"Source: /tmp/design-ai",
|
|
5599
6117
|
"Target: /tmp/claude-home",
|
|
@@ -6022,7 +6540,7 @@ def assert_workspace_json(raw: str, *, context: str, cmd: list[str]) -> None:
|
|
|
6022
6540
|
)
|
|
6023
6541
|
if workspace_context.get("packageName") != "@design-ai/cli":
|
|
6024
6542
|
raise SystemExit(f"workspace JSON after {context} packageName differs from expected package")
|
|
6025
|
-
if workspace_context.get("version") !=
|
|
6543
|
+
if workspace_context.get("version") != EXPECTED_RELEASE_VERSION:
|
|
6026
6544
|
raise SystemExit(f"workspace JSON after {context} version differs from expected release version")
|
|
6027
6545
|
for key in ("cwd", "root", "sourceRoot"):
|
|
6028
6546
|
if not isinstance(workspace_context.get(key), str) or not workspace_context[key]:
|
|
@@ -6348,6 +6866,44 @@ def assert_workspace_json(raw: str, *, context: str, cmd: list[str]) -> None:
|
|
|
6348
6866
|
if privacy.get("mutatesProfile") is not False:
|
|
6349
6867
|
raise SystemExit(f"workspace JSON after {context} learningRestoreBackups mutation privacy flag changed")
|
|
6350
6868
|
|
|
6869
|
+
retrieval_index = payload.get("retrievalIndex")
|
|
6870
|
+
if retrieval_index is not None:
|
|
6871
|
+
retrieval_index = assert_smoke_json_keys(
|
|
6872
|
+
retrieval_index,
|
|
6873
|
+
EXPECTED_WORKSPACE_RETRIEVAL_INDEX_KEYS,
|
|
6874
|
+
label="retrievalIndex",
|
|
6875
|
+
context=context,
|
|
6876
|
+
command_label="workspace JSON",
|
|
6877
|
+
)
|
|
6878
|
+
if not isinstance(retrieval_index.get("indexDir"), str) or not retrieval_index["indexDir"]:
|
|
6879
|
+
raise SystemExit(f"workspace JSON after {context} retrievalIndex indexDir is missing")
|
|
6880
|
+
if type(retrieval_index.get("fresh")) is not bool:
|
|
6881
|
+
raise SystemExit(f"workspace JSON after {context} retrievalIndex fresh is not boolean")
|
|
6882
|
+
if retrieval_index.get("status") not in ("pass", "warn"):
|
|
6883
|
+
raise SystemExit(f"workspace JSON after {context} retrievalIndex status is invalid")
|
|
6884
|
+
if (retrieval_index.get("status") == "pass") is not retrieval_index.get("fresh"):
|
|
6885
|
+
raise SystemExit(f"workspace JSON after {context} retrievalIndex status does not match freshness")
|
|
6886
|
+
if retrieval_index.get("buildCommand") != EXPECTED_INDEX_BUILD_COMMAND:
|
|
6887
|
+
raise SystemExit(f"workspace JSON after {context} retrievalIndex buildCommand differs from expected command")
|
|
6888
|
+
for section_label in EXPECTED_INDEX_FILE_BASENAMES:
|
|
6889
|
+
section = assert_smoke_json_keys(
|
|
6890
|
+
retrieval_index.get(section_label),
|
|
6891
|
+
EXPECTED_INDEX_STATUS_SECTION_KEYS,
|
|
6892
|
+
label=f"retrievalIndex {section_label}",
|
|
6893
|
+
context=context,
|
|
6894
|
+
command_label="workspace JSON",
|
|
6895
|
+
)
|
|
6896
|
+
if not isinstance(section.get("file"), str) or not section["file"]:
|
|
6897
|
+
raise SystemExit(f"workspace JSON after {context} retrievalIndex {section_label} file is missing")
|
|
6898
|
+
for key in ("present", "fresh"):
|
|
6899
|
+
if type(section.get(key)) is not bool:
|
|
6900
|
+
raise SystemExit(f"workspace JSON after {context} retrievalIndex {section_label} {key} is not boolean")
|
|
6901
|
+
if not is_lifecycle_json_non_negative_int(section.get("documentCount")):
|
|
6902
|
+
raise SystemExit(f"workspace JSON after {context} retrievalIndex {section_label} documentCount is invalid")
|
|
6903
|
+
for key in ("generatedAt", "storedDigest", "currentDigest", "error"):
|
|
6904
|
+
if not isinstance(section.get(key), str):
|
|
6905
|
+
raise SystemExit(f"workspace JSON after {context} retrievalIndex {section_label} {key} is not a string")
|
|
6906
|
+
|
|
6351
6907
|
release = assert_smoke_json_keys(
|
|
6352
6908
|
payload.get("release"),
|
|
6353
6909
|
EXPECTED_WORKSPACE_RELEASE_KEYS,
|
|
@@ -6355,7 +6911,7 @@ def assert_workspace_json(raw: str, *, context: str, cmd: list[str]) -> None:
|
|
|
6355
6911
|
context=context,
|
|
6356
6912
|
command_label="workspace JSON",
|
|
6357
6913
|
)
|
|
6358
|
-
if release.get("packageName") != "@design-ai/cli" or release.get("version") !=
|
|
6914
|
+
if release.get("packageName") != "@design-ai/cli" or release.get("version") != EXPECTED_RELEASE_VERSION:
|
|
6359
6915
|
raise SystemExit(f"workspace JSON after {context} release package metadata differs from expected values")
|
|
6360
6916
|
if not isinstance(release.get("scripts"), dict):
|
|
6361
6917
|
raise SystemExit(f"workspace JSON after {context} release scripts is not an object")
|
|
@@ -9088,6 +9644,364 @@ def run_self_test() -> None:
|
|
|
9088
9644
|
scope="smoke assertions",
|
|
9089
9645
|
)
|
|
9090
9646
|
|
|
9647
|
+
ranked_search_cmd = [
|
|
9648
|
+
"design-ai",
|
|
9649
|
+
"search",
|
|
9650
|
+
EXPECTED_CORPUS_SEARCH_QUERY,
|
|
9651
|
+
"--dir",
|
|
9652
|
+
"knowledge",
|
|
9653
|
+
"--limit",
|
|
9654
|
+
str(EXPECTED_RANKED_SEARCH_LIMIT),
|
|
9655
|
+
"--ranked",
|
|
9656
|
+
"--json",
|
|
9657
|
+
]
|
|
9658
|
+
assert_ranked_search_json(passing_ranked_search_json(), context=context, cmd=ranked_search_cmd)
|
|
9659
|
+
assert_ranked_search_json(
|
|
9660
|
+
passing_ranked_search_json(),
|
|
9661
|
+
context=context,
|
|
9662
|
+
cmd=ranked_search_cmd,
|
|
9663
|
+
expected_notice=EXPECTED_RANKED_SEARCH_NOT_BUILT_NOTICE,
|
|
9664
|
+
)
|
|
9665
|
+
expect_self_test_failure(
|
|
9666
|
+
lambda: assert_ranked_search_json("{", context=context, cmd=ranked_search_cmd),
|
|
9667
|
+
expected="failed to parse ranked search JSON",
|
|
9668
|
+
scope="smoke assertions",
|
|
9669
|
+
)
|
|
9670
|
+
assert_ranked_search_json(
|
|
9671
|
+
passing_ranked_search_json(backend="embeddings"),
|
|
9672
|
+
context=context,
|
|
9673
|
+
cmd=ranked_search_cmd,
|
|
9674
|
+
expected_backend="embeddings",
|
|
9675
|
+
)
|
|
9676
|
+
ranked_wrong_backend = json.loads(passing_ranked_search_json(backend="embeddings"))
|
|
9677
|
+
ranked_wrong_backend["backend"] = "lexical"
|
|
9678
|
+
expect_self_test_failure(
|
|
9679
|
+
lambda: assert_ranked_search_json(
|
|
9680
|
+
json.dumps(ranked_wrong_backend), context=context, cmd=ranked_search_cmd, expected_backend="embeddings"
|
|
9681
|
+
),
|
|
9682
|
+
expected="backend differs from expected backend",
|
|
9683
|
+
scope="smoke assertions",
|
|
9684
|
+
)
|
|
9685
|
+
ranked_wrong_query = json.loads(passing_ranked_search_json())
|
|
9686
|
+
ranked_wrong_query["query"] = "Inter"
|
|
9687
|
+
expect_self_test_failure(
|
|
9688
|
+
lambda: assert_ranked_search_json(json.dumps(ranked_wrong_query), context=context, cmd=ranked_search_cmd),
|
|
9689
|
+
expected="query differs",
|
|
9690
|
+
scope="smoke assertions",
|
|
9691
|
+
)
|
|
9692
|
+
ranked_flag_false = json.loads(passing_ranked_search_json())
|
|
9693
|
+
ranked_flag_false["ranked"] = False
|
|
9694
|
+
expect_self_test_failure(
|
|
9695
|
+
lambda: assert_ranked_search_json(json.dumps(ranked_flag_false), context=context, cmd=ranked_search_cmd),
|
|
9696
|
+
expected="ranked flag is not true",
|
|
9697
|
+
scope="smoke assertions",
|
|
9698
|
+
)
|
|
9699
|
+
ranked_wrong_notice = json.loads(passing_ranked_search_json())
|
|
9700
|
+
ranked_wrong_notice["notice"] = "corpus index is stale"
|
|
9701
|
+
expect_self_test_failure(
|
|
9702
|
+
lambda: assert_ranked_search_json(
|
|
9703
|
+
json.dumps(ranked_wrong_notice),
|
|
9704
|
+
context=context,
|
|
9705
|
+
cmd=ranked_search_cmd,
|
|
9706
|
+
expected_notice=EXPECTED_RANKED_SEARCH_NOT_BUILT_NOTICE,
|
|
9707
|
+
),
|
|
9708
|
+
expected="notice differs",
|
|
9709
|
+
scope="smoke assertions",
|
|
9710
|
+
)
|
|
9711
|
+
ranked_no_hits = json.loads(passing_ranked_search_json())
|
|
9712
|
+
ranked_no_hits["hits"] = []
|
|
9713
|
+
expect_self_test_failure(
|
|
9714
|
+
lambda: assert_ranked_search_json(json.dumps(ranked_no_hits), context=context, cmd=ranked_search_cmd),
|
|
9715
|
+
expected="does not contain any hits",
|
|
9716
|
+
scope="smoke assertions",
|
|
9717
|
+
)
|
|
9718
|
+
ranked_extra_hits = json.loads(passing_ranked_search_json())
|
|
9719
|
+
while len(ranked_extra_hits["hits"]) <= EXPECTED_RANKED_SEARCH_LIMIT:
|
|
9720
|
+
ranked_extra_hits["hits"].append(dict(ranked_extra_hits["hits"][0]))
|
|
9721
|
+
expect_self_test_failure(
|
|
9722
|
+
lambda: assert_ranked_search_json(json.dumps(ranked_extra_hits), context=context, cmd=ranked_search_cmd),
|
|
9723
|
+
expected="hit count exceeds",
|
|
9724
|
+
scope="smoke assertions",
|
|
9725
|
+
)
|
|
9726
|
+
ranked_zero_score = json.loads(passing_ranked_search_json())
|
|
9727
|
+
ranked_zero_score["hits"][0]["score"] = 0
|
|
9728
|
+
expect_self_test_failure(
|
|
9729
|
+
lambda: assert_ranked_search_json(json.dumps(ranked_zero_score), context=context, cmd=ranked_search_cmd),
|
|
9730
|
+
expected="score is not a positive number",
|
|
9731
|
+
scope="smoke assertions",
|
|
9732
|
+
)
|
|
9733
|
+
ranked_bool_score = json.loads(passing_ranked_search_json())
|
|
9734
|
+
ranked_bool_score["hits"][0]["score"] = True
|
|
9735
|
+
expect_self_test_failure(
|
|
9736
|
+
lambda: assert_ranked_search_json(json.dumps(ranked_bool_score), context=context, cmd=ranked_search_cmd),
|
|
9737
|
+
expected="score is not a positive number",
|
|
9738
|
+
scope="smoke assertions",
|
|
9739
|
+
)
|
|
9740
|
+
ranked_empty_tokens = json.loads(passing_ranked_search_json())
|
|
9741
|
+
ranked_empty_tokens["hits"][0]["matchedTokens"] = []
|
|
9742
|
+
expect_self_test_failure(
|
|
9743
|
+
lambda: assert_ranked_search_json(json.dumps(ranked_empty_tokens), context=context, cmd=ranked_search_cmd),
|
|
9744
|
+
expected="matchedTokens is empty",
|
|
9745
|
+
scope="smoke assertions",
|
|
9746
|
+
)
|
|
9747
|
+
ranked_unmatched_tokens = json.loads(passing_ranked_search_json())
|
|
9748
|
+
ranked_unmatched_tokens["hits"][0]["matchedTokens"] = ["inter"]
|
|
9749
|
+
expect_self_test_failure(
|
|
9750
|
+
lambda: assert_ranked_search_json(json.dumps(ranked_unmatched_tokens), context=context, cmd=ranked_search_cmd),
|
|
9751
|
+
expected="missing a hit matching the query token",
|
|
9752
|
+
scope="smoke assertions",
|
|
9753
|
+
)
|
|
9754
|
+
ranked_missing_hit_key = json.loads(passing_ranked_search_json())
|
|
9755
|
+
del ranked_missing_hit_key["hits"][0]["preview"]
|
|
9756
|
+
expect_self_test_failure(
|
|
9757
|
+
lambda: assert_ranked_search_json(json.dumps(ranked_missing_hit_key), context=context, cmd=ranked_search_cmd),
|
|
9758
|
+
expected="hit keys changed",
|
|
9759
|
+
scope="smoke assertions",
|
|
9760
|
+
)
|
|
9761
|
+
ranked_relative_file = json.loads(passing_ranked_search_json())
|
|
9762
|
+
ranked_relative_file["hits"][0]["file"] = EXPECTED_CORPUS_SEARCH_HIT
|
|
9763
|
+
expect_self_test_failure(
|
|
9764
|
+
lambda: assert_ranked_search_json(json.dumps(ranked_relative_file), context=context, cmd=ranked_search_cmd),
|
|
9765
|
+
expected="file is not absolute",
|
|
9766
|
+
scope="smoke assertions",
|
|
9767
|
+
)
|
|
9768
|
+
expect_self_test_failure(
|
|
9769
|
+
lambda: assert_ranked_search_json("\x1b[31m{}", context=context, cmd=ranked_search_cmd),
|
|
9770
|
+
expected="ANSI escape",
|
|
9771
|
+
scope="smoke assertions",
|
|
9772
|
+
)
|
|
9773
|
+
assert_ranked_search_determinism(
|
|
9774
|
+
passing_ranked_search_json(),
|
|
9775
|
+
passing_ranked_search_json(),
|
|
9776
|
+
context=context,
|
|
9777
|
+
cmd=ranked_search_cmd,
|
|
9778
|
+
)
|
|
9779
|
+
expect_self_test_failure(
|
|
9780
|
+
lambda: assert_ranked_search_determinism(
|
|
9781
|
+
passing_ranked_search_json(),
|
|
9782
|
+
passing_ranked_search_json() + "\n",
|
|
9783
|
+
context=context,
|
|
9784
|
+
cmd=ranked_search_cmd,
|
|
9785
|
+
),
|
|
9786
|
+
expected="differs between identical runs",
|
|
9787
|
+
scope="smoke assertions",
|
|
9788
|
+
)
|
|
9789
|
+
expect_self_test_failure(
|
|
9790
|
+
lambda: assert_ranked_search_determinism(
|
|
9791
|
+
"\x1b[31m{}",
|
|
9792
|
+
"\x1b[31m{}",
|
|
9793
|
+
context=context,
|
|
9794
|
+
cmd=ranked_search_cmd,
|
|
9795
|
+
),
|
|
9796
|
+
expected="ANSI escape",
|
|
9797
|
+
scope="smoke assertions",
|
|
9798
|
+
)
|
|
9799
|
+
|
|
9800
|
+
index_dir = "/tmp/design-ai-index"
|
|
9801
|
+
index_build_cmd = ["design-ai", "index", "--build", "--json"]
|
|
9802
|
+
assert_index_build_json(passing_index_build_json(index_dir), index_dir=index_dir, context=context, cmd=index_build_cmd)
|
|
9803
|
+
expect_self_test_failure(
|
|
9804
|
+
lambda: assert_index_build_json("{", index_dir=index_dir, context=context, cmd=index_build_cmd),
|
|
9805
|
+
expected="failed to parse index build JSON",
|
|
9806
|
+
scope="smoke assertions",
|
|
9807
|
+
)
|
|
9808
|
+
index_build_wrong_action = json.loads(passing_index_build_json(index_dir))
|
|
9809
|
+
index_build_wrong_action["action"] = "status"
|
|
9810
|
+
expect_self_test_failure(
|
|
9811
|
+
lambda: assert_index_build_json(
|
|
9812
|
+
json.dumps(index_build_wrong_action), index_dir=index_dir, context=context, cmd=index_build_cmd
|
|
9813
|
+
),
|
|
9814
|
+
expected="action differs from build",
|
|
9815
|
+
scope="smoke assertions",
|
|
9816
|
+
)
|
|
9817
|
+
expect_self_test_failure(
|
|
9818
|
+
lambda: assert_index_build_json(
|
|
9819
|
+
passing_index_build_json("/tmp/other-index"), index_dir=index_dir, context=context, cmd=index_build_cmd
|
|
9820
|
+
),
|
|
9821
|
+
expected="indexDir differs",
|
|
9822
|
+
scope="smoke assertions",
|
|
9823
|
+
)
|
|
9824
|
+
index_build_zero_documents = json.loads(passing_index_build_json(index_dir))
|
|
9825
|
+
index_build_zero_documents["corpus"]["documentCount"] = 0
|
|
9826
|
+
expect_self_test_failure(
|
|
9827
|
+
lambda: assert_index_build_json(
|
|
9828
|
+
json.dumps(index_build_zero_documents), index_dir=index_dir, context=context, cmd=index_build_cmd
|
|
9829
|
+
),
|
|
9830
|
+
expected="documentCount is not a positive integer",
|
|
9831
|
+
scope="smoke assertions",
|
|
9832
|
+
)
|
|
9833
|
+
index_build_bad_digest = json.loads(passing_index_build_json(index_dir))
|
|
9834
|
+
index_build_bad_digest["learning"]["digest"] = "md5:abc"
|
|
9835
|
+
expect_self_test_failure(
|
|
9836
|
+
lambda: assert_index_build_json(
|
|
9837
|
+
json.dumps(index_build_bad_digest), index_dir=index_dir, context=context, cmd=index_build_cmd
|
|
9838
|
+
),
|
|
9839
|
+
expected="digest is not a sha256 digest",
|
|
9840
|
+
scope="smoke assertions",
|
|
9841
|
+
)
|
|
9842
|
+
index_build_missing_key = json.loads(passing_index_build_json(index_dir))
|
|
9843
|
+
del index_build_missing_key["corpus"]["file"]
|
|
9844
|
+
expect_self_test_failure(
|
|
9845
|
+
lambda: assert_index_build_json(
|
|
9846
|
+
json.dumps(index_build_missing_key), index_dir=index_dir, context=context, cmd=index_build_cmd
|
|
9847
|
+
),
|
|
9848
|
+
expected="corpus keys changed",
|
|
9849
|
+
scope="smoke assertions",
|
|
9850
|
+
)
|
|
9851
|
+
expect_self_test_failure(
|
|
9852
|
+
lambda: assert_index_build_json("\x1b[31m{}", index_dir=index_dir, context=context, cmd=index_build_cmd),
|
|
9853
|
+
expected="ANSI escape",
|
|
9854
|
+
scope="smoke assertions",
|
|
9855
|
+
)
|
|
9856
|
+
|
|
9857
|
+
index_status_cmd = ["design-ai", "index", "--status", "--json"]
|
|
9858
|
+
assert_index_status_json(passing_index_status_json(index_dir), index_dir=index_dir, context=context, cmd=index_status_cmd)
|
|
9859
|
+
expect_self_test_failure(
|
|
9860
|
+
lambda: assert_index_status_json("{", index_dir=index_dir, context=context, cmd=index_status_cmd),
|
|
9861
|
+
expected="failed to parse index status JSON",
|
|
9862
|
+
scope="smoke assertions",
|
|
9863
|
+
)
|
|
9864
|
+
index_status_stale = json.loads(passing_index_status_json(index_dir))
|
|
9865
|
+
index_status_stale["fresh"] = False
|
|
9866
|
+
expect_self_test_failure(
|
|
9867
|
+
lambda: assert_index_status_json(
|
|
9868
|
+
json.dumps(index_status_stale), index_dir=index_dir, context=context, cmd=index_status_cmd
|
|
9869
|
+
),
|
|
9870
|
+
expected="is not fresh",
|
|
9871
|
+
scope="smoke assertions",
|
|
9872
|
+
)
|
|
9873
|
+
index_status_wrong_command = json.loads(passing_index_status_json(index_dir))
|
|
9874
|
+
index_status_wrong_command["buildCommand"] = "design-ai rebuild"
|
|
9875
|
+
expect_self_test_failure(
|
|
9876
|
+
lambda: assert_index_status_json(
|
|
9877
|
+
json.dumps(index_status_wrong_command), index_dir=index_dir, context=context, cmd=index_status_cmd
|
|
9878
|
+
),
|
|
9879
|
+
expected="buildCommand differs",
|
|
9880
|
+
scope="smoke assertions",
|
|
9881
|
+
)
|
|
9882
|
+
index_status_missing_section = json.loads(passing_index_status_json(index_dir))
|
|
9883
|
+
index_status_missing_section["corpus"]["present"] = False
|
|
9884
|
+
expect_self_test_failure(
|
|
9885
|
+
lambda: assert_index_status_json(
|
|
9886
|
+
json.dumps(index_status_missing_section), index_dir=index_dir, context=context, cmd=index_status_cmd
|
|
9887
|
+
),
|
|
9888
|
+
expected="corpus index is not present",
|
|
9889
|
+
scope="smoke assertions",
|
|
9890
|
+
)
|
|
9891
|
+
index_status_stale_section = json.loads(passing_index_status_json(index_dir))
|
|
9892
|
+
index_status_stale_section["learning"]["fresh"] = False
|
|
9893
|
+
expect_self_test_failure(
|
|
9894
|
+
lambda: assert_index_status_json(
|
|
9895
|
+
json.dumps(index_status_stale_section), index_dir=index_dir, context=context, cmd=index_status_cmd
|
|
9896
|
+
),
|
|
9897
|
+
expected="learning index is not fresh",
|
|
9898
|
+
scope="smoke assertions",
|
|
9899
|
+
)
|
|
9900
|
+
index_status_digest_drift = json.loads(passing_index_status_json(index_dir))
|
|
9901
|
+
index_status_digest_drift["corpus"]["currentDigest"] = f"sha256:{'2' * 64}"
|
|
9902
|
+
expect_self_test_failure(
|
|
9903
|
+
lambda: assert_index_status_json(
|
|
9904
|
+
json.dumps(index_status_digest_drift), index_dir=index_dir, context=context, cmd=index_status_cmd
|
|
9905
|
+
),
|
|
9906
|
+
expected="stored digest differs from current digest",
|
|
9907
|
+
scope="smoke assertions",
|
|
9908
|
+
)
|
|
9909
|
+
index_status_error = json.loads(passing_index_status_json(index_dir))
|
|
9910
|
+
index_status_error["learning"]["error"] = "unreadable"
|
|
9911
|
+
expect_self_test_failure(
|
|
9912
|
+
lambda: assert_index_status_json(
|
|
9913
|
+
json.dumps(index_status_error), index_dir=index_dir, context=context, cmd=index_status_cmd
|
|
9914
|
+
),
|
|
9915
|
+
expected="error is not empty",
|
|
9916
|
+
scope="smoke assertions",
|
|
9917
|
+
)
|
|
9918
|
+
index_status_embeddings_present_unexpectedly = json.loads(passing_index_status_json(index_dir, with_embeddings=True))
|
|
9919
|
+
expect_self_test_failure(
|
|
9920
|
+
lambda: assert_index_status_json(
|
|
9921
|
+
json.dumps(index_status_embeddings_present_unexpectedly), index_dir=index_dir, context=context, cmd=index_status_cmd
|
|
9922
|
+
),
|
|
9923
|
+
expected="embeddings is not null when embeddings were not built",
|
|
9924
|
+
scope="smoke assertions",
|
|
9925
|
+
)
|
|
9926
|
+
|
|
9927
|
+
assert_index_status_json(
|
|
9928
|
+
passing_index_status_json(index_dir, with_embeddings=True),
|
|
9929
|
+
index_dir=index_dir,
|
|
9930
|
+
context=context,
|
|
9931
|
+
cmd=index_status_cmd,
|
|
9932
|
+
expect_embeddings=True,
|
|
9933
|
+
)
|
|
9934
|
+
index_status_embeddings_stale = json.loads(passing_index_status_json(index_dir, with_embeddings=True))
|
|
9935
|
+
index_status_embeddings_stale["embeddings"]["fresh"] = False
|
|
9936
|
+
expect_self_test_failure(
|
|
9937
|
+
lambda: assert_index_status_json(
|
|
9938
|
+
json.dumps(index_status_embeddings_stale),
|
|
9939
|
+
index_dir=index_dir,
|
|
9940
|
+
context=context,
|
|
9941
|
+
cmd=index_status_cmd,
|
|
9942
|
+
expect_embeddings=True,
|
|
9943
|
+
),
|
|
9944
|
+
expected="embeddings index is not fresh",
|
|
9945
|
+
scope="smoke assertions",
|
|
9946
|
+
)
|
|
9947
|
+
index_status_embeddings_no_provider_command = json.loads(passing_index_status_json(index_dir, with_embeddings=True))
|
|
9948
|
+
index_status_embeddings_no_provider_command["embeddings"]["provider"] = {"command": "", "modelLabel": "", "dimensions": 3}
|
|
9949
|
+
expect_self_test_failure(
|
|
9950
|
+
lambda: assert_index_status_json(
|
|
9951
|
+
json.dumps(index_status_embeddings_no_provider_command),
|
|
9952
|
+
index_dir=index_dir,
|
|
9953
|
+
context=context,
|
|
9954
|
+
cmd=index_status_cmd,
|
|
9955
|
+
expect_embeddings=True,
|
|
9956
|
+
),
|
|
9957
|
+
expected="embeddings provider is missing a command",
|
|
9958
|
+
scope="smoke assertions",
|
|
9959
|
+
)
|
|
9960
|
+
|
|
9961
|
+
index_verify_cmd = ["design-ai", "index", "--verify", "--json"]
|
|
9962
|
+
assert_index_verify_json(passing_index_verify_json(index_dir), index_dir=index_dir, context=context, cmd=index_verify_cmd)
|
|
9963
|
+
expect_self_test_failure(
|
|
9964
|
+
lambda: assert_index_verify_json("{", index_dir=index_dir, context=context, cmd=index_verify_cmd),
|
|
9965
|
+
expected="failed to parse index verify JSON",
|
|
9966
|
+
scope="smoke assertions",
|
|
9967
|
+
)
|
|
9968
|
+
index_verify_not_ok = json.loads(passing_index_verify_json(index_dir))
|
|
9969
|
+
index_verify_not_ok["ok"] = False
|
|
9970
|
+
expect_self_test_failure(
|
|
9971
|
+
lambda: assert_index_verify_json(
|
|
9972
|
+
json.dumps(index_verify_not_ok), index_dir=index_dir, context=context, cmd=index_verify_cmd
|
|
9973
|
+
),
|
|
9974
|
+
expected="ok is not true",
|
|
9975
|
+
scope="smoke assertions",
|
|
9976
|
+
)
|
|
9977
|
+
index_verify_wrong_names = json.loads(passing_index_verify_json(index_dir))
|
|
9978
|
+
index_verify_wrong_names["checks"] = index_verify_wrong_names["checks"][:1]
|
|
9979
|
+
expect_self_test_failure(
|
|
9980
|
+
lambda: assert_index_verify_json(
|
|
9981
|
+
json.dumps(index_verify_wrong_names), index_dir=index_dir, context=context, cmd=index_verify_cmd
|
|
9982
|
+
),
|
|
9983
|
+
expected="check names changed",
|
|
9984
|
+
scope="smoke assertions",
|
|
9985
|
+
)
|
|
9986
|
+
index_verify_mismatch = json.loads(passing_index_verify_json(index_dir))
|
|
9987
|
+
index_verify_mismatch["checks"][1]["matches"] = False
|
|
9988
|
+
expect_self_test_failure(
|
|
9989
|
+
lambda: assert_index_verify_json(
|
|
9990
|
+
json.dumps(index_verify_mismatch), index_dir=index_dir, context=context, cmd=index_verify_cmd
|
|
9991
|
+
),
|
|
9992
|
+
expected="learning check does not match",
|
|
9993
|
+
scope="smoke assertions",
|
|
9994
|
+
)
|
|
9995
|
+
index_verify_reason = json.loads(passing_index_verify_json(index_dir))
|
|
9996
|
+
index_verify_reason["checks"][0]["reason"] = "digest drift"
|
|
9997
|
+
expect_self_test_failure(
|
|
9998
|
+
lambda: assert_index_verify_json(
|
|
9999
|
+
json.dumps(index_verify_reason), index_dir=index_dir, context=context, cmd=index_verify_cmd
|
|
10000
|
+
),
|
|
10001
|
+
expected="corpus check reason is not empty",
|
|
10002
|
+
scope="smoke assertions",
|
|
10003
|
+
)
|
|
10004
|
+
|
|
9091
10005
|
show_cmd = ["design-ai", "show", EXPECTED_CORPUS_SHOW_TARGET, "--context", "0", "--json"]
|
|
9092
10006
|
assert_show_json_line(passing_show_json(), context=context, cmd=show_cmd)
|
|
9093
10007
|
expect_self_test_failure(
|
|
@@ -10780,7 +11694,7 @@ def run_self_test() -> None:
|
|
|
10780
11694
|
)
|
|
10781
11695
|
expect_self_test_failure(
|
|
10782
11696
|
lambda: assert_version_json(
|
|
10783
|
-
passing_version_json().replace('"plugin": "
|
|
11697
|
+
passing_version_json().replace(f'"plugin": "{EXPECTED_RELEASE_VERSION}"', '"plugin": "unknown"'),
|
|
10784
11698
|
context=context,
|
|
10785
11699
|
cmd=[*version_cmd, "--json"],
|
|
10786
11700
|
),
|
|
@@ -10951,6 +11865,54 @@ def run_self_test() -> None:
|
|
|
10951
11865
|
expected="privacy flags changed",
|
|
10952
11866
|
scope="smoke assertions",
|
|
10953
11867
|
)
|
|
11868
|
+
workspace_retrieval_index_payload = json.loads(passing_workspace_json())
|
|
11869
|
+
workspace_retrieval_index_payload["retrievalIndex"] = {
|
|
11870
|
+
"indexDir": "/tmp/design-ai-index",
|
|
11871
|
+
"corpus": json.loads(passing_index_status_json())["corpus"],
|
|
11872
|
+
"learning": json.loads(passing_index_status_json())["learning"],
|
|
11873
|
+
"fresh": True,
|
|
11874
|
+
"status": "pass",
|
|
11875
|
+
"buildCommand": EXPECTED_INDEX_BUILD_COMMAND,
|
|
11876
|
+
}
|
|
11877
|
+
assert_workspace_json(json.dumps(workspace_retrieval_index_payload), context=context, cmd=workspace_cmd)
|
|
11878
|
+
stale_retrieval_index_payload = json.loads(json.dumps(workspace_retrieval_index_payload))
|
|
11879
|
+
stale_retrieval_index_payload["retrievalIndex"]["fresh"] = False
|
|
11880
|
+
stale_retrieval_index_payload["retrievalIndex"]["status"] = "warn"
|
|
11881
|
+
stale_retrieval_index_payload["retrievalIndex"]["corpus"]["fresh"] = False
|
|
11882
|
+
assert_workspace_json(json.dumps(stale_retrieval_index_payload), context=context, cmd=workspace_cmd)
|
|
11883
|
+
mismatched_retrieval_index_payload = json.loads(json.dumps(workspace_retrieval_index_payload))
|
|
11884
|
+
mismatched_retrieval_index_payload["retrievalIndex"]["status"] = "warn"
|
|
11885
|
+
expect_self_test_failure(
|
|
11886
|
+
lambda: assert_workspace_json(
|
|
11887
|
+
json.dumps(mismatched_retrieval_index_payload),
|
|
11888
|
+
context=context,
|
|
11889
|
+
cmd=workspace_cmd,
|
|
11890
|
+
),
|
|
11891
|
+
expected="retrievalIndex status does not match freshness",
|
|
11892
|
+
scope="smoke assertions",
|
|
11893
|
+
)
|
|
11894
|
+
wrong_build_command_retrieval_index_payload = json.loads(json.dumps(workspace_retrieval_index_payload))
|
|
11895
|
+
wrong_build_command_retrieval_index_payload["retrievalIndex"]["buildCommand"] = "design-ai rebuild"
|
|
11896
|
+
expect_self_test_failure(
|
|
11897
|
+
lambda: assert_workspace_json(
|
|
11898
|
+
json.dumps(wrong_build_command_retrieval_index_payload),
|
|
11899
|
+
context=context,
|
|
11900
|
+
cmd=workspace_cmd,
|
|
11901
|
+
),
|
|
11902
|
+
expected="retrievalIndex buildCommand differs",
|
|
11903
|
+
scope="smoke assertions",
|
|
11904
|
+
)
|
|
11905
|
+
missing_retrieval_index_key_payload = json.loads(json.dumps(workspace_retrieval_index_payload))
|
|
11906
|
+
del missing_retrieval_index_key_payload["retrievalIndex"]["learning"]["documentCount"]
|
|
11907
|
+
expect_self_test_failure(
|
|
11908
|
+
lambda: assert_workspace_json(
|
|
11909
|
+
json.dumps(missing_retrieval_index_key_payload),
|
|
11910
|
+
context=context,
|
|
11911
|
+
cmd=workspace_cmd,
|
|
11912
|
+
),
|
|
11913
|
+
expected="retrievalIndex learning keys changed",
|
|
11914
|
+
scope="smoke assertions",
|
|
11915
|
+
)
|
|
10954
11916
|
assert_workspace_strict_failure_json(
|
|
10955
11917
|
passing_workspace_json(),
|
|
10956
11918
|
returncode=1,
|
|
@@ -11007,6 +11969,7 @@ def run_self_test() -> None:
|
|
|
11007
11969
|
"learningUsage": reordered_workspace_payload["learningUsage"],
|
|
11008
11970
|
"learningEval": reordered_workspace_payload["learningEval"],
|
|
11009
11971
|
"learningRestoreBackups": reordered_workspace_payload["learningRestoreBackups"],
|
|
11972
|
+
"retrievalIndex": reordered_workspace_payload["retrievalIndex"],
|
|
11010
11973
|
"release": reordered_workspace_payload["release"],
|
|
11011
11974
|
"nextActions": reordered_workspace_payload["nextActions"],
|
|
11012
11975
|
}
|