@design-ai/cli 4.56.0 → 4.58.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 +68 -0
- package/README.ko.md +11 -28
- package/README.md +12 -25
- 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/route.mjs +13 -1
- 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/route.mjs +62 -5
- package/cli/lib/search-ranked.mjs +202 -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 +82 -8
- package/docs/external-status.md +9 -11
- 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 +12 -9
- package/knowledge/components/INDEX.md +202 -202
- package/knowledge/i18n/korean-density-conventions.md +108 -0
- package/knowledge/i18n/korean-product-conventions.md +2 -0
- package/knowledge/patterns/async-control.md +233 -0
- package/knowledge/patterns/brand-references.md +72 -72
- package/knowledge/patterns/error-states.md +1 -0
- package/knowledge/patterns/form-design.md +2 -0
- 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 +537 -3
- package/tools/audit/release-metadata.py +47 -1
- package/tools/audit/smoke_assertions.py +918 -13
- package/tools/migrations/refs-links-to-reference-pages.py +179 -0
|
@@ -115,6 +115,7 @@ EXPECTED_HELP_TOPICS = (
|
|
|
115
115
|
"status",
|
|
116
116
|
"list",
|
|
117
117
|
"search",
|
|
118
|
+
"index",
|
|
118
119
|
"show",
|
|
119
120
|
"route",
|
|
120
121
|
"routes",
|
|
@@ -159,17 +160,18 @@ EXPECTED_HELP_TOPIC_USAGES = {
|
|
|
159
160
|
"uninstall": "design-ai uninstall [--json]",
|
|
160
161
|
"status": "design-ai status [--json]",
|
|
161
162
|
"list": "design-ai list [skills|commands|agents] [--json]",
|
|
162
|
-
"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\"]]",
|
|
163
165
|
"show": "design-ai show <file[:line]> [--lines N:M] [--context N] [--json]",
|
|
164
166
|
"route": "design-ai route <brief|--from-file file|--stdin|--list|--eval-template|--eval> [--limit N]",
|
|
165
167
|
"routes": "design-ai routes [--json]",
|
|
166
|
-
"prompt": "design-ai prompt <brief|--from-file file|--stdin|--eval-template|--eval> [--route id] [--with-learning] [--learning-category kind] [--learning-limit N] [--out file]",
|
|
167
|
-
"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]",
|
|
168
170
|
"check": "design-ai check <artifact.md|--stdin|--examples> [--route id|--all-routes] [--learn]",
|
|
169
171
|
"audit": "design-ai audit [--strict] [--quiet] [--json]",
|
|
170
172
|
"doctor": "design-ai doctor [--strict] [--json] [--fix]",
|
|
171
173
|
"examples": "design-ai examples [query] [--route id] [--limit N] [--json]",
|
|
172
|
-
"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]",
|
|
173
175
|
"workspace": "design-ai workspace [--root path] [--learning-file path] [--learning-usage path] [--learning-eval path] [--strict] [--json]",
|
|
174
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]",
|
|
175
177
|
"mcp": "design-ai mcp",
|
|
@@ -182,19 +184,20 @@ EXPECTED_HELP_TOPIC_FRAGMENTS = {
|
|
|
182
184
|
"uninstall": ("Usage:", "design-ai uninstall [--json]"),
|
|
183
185
|
"status": ("Usage:", "design-ai status [--json]"),
|
|
184
186
|
"list": ("Usage:", "design-ai list [skills|commands|agents]"),
|
|
185
|
-
"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\"]]"),
|
|
186
189
|
"show": ("Usage:", "design-ai show <file[:line|start-end]> [--lines N:M] [--context N] [--json]"),
|
|
187
190
|
"route": ("Usage:", "design-ai route <brief>", "design-ai route --list [--json]", "design-ai route --eval-template [--json]"),
|
|
188
191
|
"routes": ("Usage:", "design-ai routes [--json]", "Equivalent to: design-ai route --list"),
|
|
189
192
|
"prompt": (
|
|
190
193
|
"Usage:",
|
|
191
|
-
"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]",
|
|
192
195
|
"design-ai prompt --eval-template [--json] [--out file] [--force]",
|
|
193
196
|
"design-ai prompt --eval --from-file prompt-eval.json [--strict] [--json] [--out file] [--force]",
|
|
194
197
|
),
|
|
195
198
|
"pack": (
|
|
196
199
|
"Usage:",
|
|
197
|
-
"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]",
|
|
198
201
|
"design-ai pack --eval-template [--json] [--out file] [--force]",
|
|
199
202
|
"design-ai pack --eval --from-file pack-eval.json [--strict] [--json] [--out file] [--force]",
|
|
200
203
|
),
|
|
@@ -205,6 +208,8 @@ EXPECTED_HELP_TOPIC_FRAGMENTS = {
|
|
|
205
208
|
"learn": (
|
|
206
209
|
"Usage:",
|
|
207
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",
|
|
208
213
|
"design-ai learn --init [--yes|--dry-run] [--json] [--out file] [--force]",
|
|
209
214
|
"design-ai learn --feedback text [--outcome keep|improve|avoid] [--category kind] [--json] [--out file] [--force]",
|
|
210
215
|
"design-ai learn --feedback --from-file notes.md [--outcome keep|improve|avoid] [--category kind] [--json] [--out file] [--force]",
|
|
@@ -368,7 +373,7 @@ EXPECTED_MAIN_HELP_FRAGMENTS = (
|
|
|
368
373
|
"pack <brief|--from-file file|--stdin|--eval-template|--eval>",
|
|
369
374
|
"check <artifact.md|--stdin|--examples>",
|
|
370
375
|
"examples [query]",
|
|
371
|
-
"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]",
|
|
372
377
|
"workspace [--root path] [--learning-file path] [--learning-usage path] [--learning-eval path] [--strict] [--json]",
|
|
373
378
|
"site <workspace.json|--stdin>",
|
|
374
379
|
"site --init",
|
|
@@ -576,6 +581,39 @@ EXPECTED_ERROR_PREFIX = "\u2717"
|
|
|
576
581
|
EXPECTED_CORPUS_SEARCH_QUERY = "Pretendard"
|
|
577
582
|
EXPECTED_CORPUS_SEARCH_HIT = "knowledge/PRINCIPLES.md"
|
|
578
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
|
+
]
|
|
579
617
|
EXPECTED_CORPUS_SHOW_TARGET = "knowledge/PRINCIPLES.md:1"
|
|
580
618
|
EXPECTED_CORPUS_SHOW_REL_PATH = "knowledge/PRINCIPLES.md"
|
|
581
619
|
EXPECTED_CORPUS_SHOW_TEXT = "<!-- hand-written -->"
|
|
@@ -804,7 +842,8 @@ EXPECTED_CHECK_EXAMPLES_PAYLOAD_KEYS = [
|
|
|
804
842
|
"examples",
|
|
805
843
|
]
|
|
806
844
|
EXPECTED_CHECK_EXAMPLE_ENTRY_KEYS = ["example", "report"]
|
|
807
|
-
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"]
|
|
808
847
|
EXPECTED_WORKSPACE_CONTEXT_KEYS = ["cwd", "root", "sourceRoot", "packageName", "version"]
|
|
809
848
|
EXPECTED_WORKSPACE_GIT_KEYS = [
|
|
810
849
|
"isRepo",
|
|
@@ -1932,6 +1971,105 @@ def passing_search_human_output() -> str:
|
|
|
1932
1971
|
])
|
|
1933
1972
|
|
|
1934
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
|
+
|
|
1935
2073
|
def passing_show_json() -> str:
|
|
1936
2074
|
return json.dumps({
|
|
1937
2075
|
"file": "/tmp/design-ai/knowledge/PRINCIPLES.md",
|
|
@@ -2108,6 +2246,7 @@ def passing_route_explain_human_output() -> str:
|
|
|
2108
2246
|
f" agent: ✓ {EXPECTED_ROUTE_AGENT}",
|
|
2109
2247
|
" read: ✓ knowledge/PRINCIPLES.md",
|
|
2110
2248
|
f" read: ✓ {EXPECTED_ROUTE_KNOWLEDGE}",
|
|
2249
|
+
" related: knowledge/patterns/design-system-qa.md (score 10.13)",
|
|
2111
2250
|
"",
|
|
2112
2251
|
])
|
|
2113
2252
|
|
|
@@ -2667,6 +2806,321 @@ def assert_search_human_output(raw: str, *, context: str, cmd: list[str]) -> Non
|
|
|
2667
2806
|
)
|
|
2668
2807
|
|
|
2669
2808
|
|
|
2809
|
+
def is_ranked_search_positive_score(value: object) -> bool:
|
|
2810
|
+
return type(value) in (int, float) and value > 0
|
|
2811
|
+
|
|
2812
|
+
|
|
2813
|
+
def is_index_sha256_digest(value: object) -> bool:
|
|
2814
|
+
if not isinstance(value, str) or not value.startswith("sha256:"):
|
|
2815
|
+
return False
|
|
2816
|
+
digest = value[len("sha256:"):]
|
|
2817
|
+
return len(digest) == 64 and all(char in "0123456789abcdef" for char in digest)
|
|
2818
|
+
|
|
2819
|
+
|
|
2820
|
+
def assert_index_file_path(
|
|
2821
|
+
value: object,
|
|
2822
|
+
*,
|
|
2823
|
+
index_dir: str,
|
|
2824
|
+
label: str,
|
|
2825
|
+
context: str,
|
|
2826
|
+
command_label: str,
|
|
2827
|
+
) -> None:
|
|
2828
|
+
expected = str(Path(index_dir) / EXPECTED_INDEX_FILE_BASENAMES[label])
|
|
2829
|
+
if value != expected:
|
|
2830
|
+
raise SystemExit(f"{command_label} after {context} {label} file differs from expected index path")
|
|
2831
|
+
|
|
2832
|
+
|
|
2833
|
+
def assert_ranked_search_json(
|
|
2834
|
+
raw: str,
|
|
2835
|
+
*,
|
|
2836
|
+
context: str,
|
|
2837
|
+
cmd: list[str],
|
|
2838
|
+
expected_notice: str | None = None,
|
|
2839
|
+
expected_backend: str | None = None,
|
|
2840
|
+
) -> None:
|
|
2841
|
+
assert_no_ansi(raw, cmd)
|
|
2842
|
+
try:
|
|
2843
|
+
payload = json.loads(raw)
|
|
2844
|
+
except json.JSONDecodeError as error:
|
|
2845
|
+
raise SystemExit(f"failed to parse ranked search JSON after {context}") from error
|
|
2846
|
+
|
|
2847
|
+
expected_keys = ["query", "ranked", "notice", "hits"]
|
|
2848
|
+
if expected_backend is not None:
|
|
2849
|
+
expected_keys.append("backend")
|
|
2850
|
+
payload = assert_corpus_json_keys(
|
|
2851
|
+
payload,
|
|
2852
|
+
expected_keys,
|
|
2853
|
+
label="top-level",
|
|
2854
|
+
context=context,
|
|
2855
|
+
command_label="ranked search JSON",
|
|
2856
|
+
)
|
|
2857
|
+
|
|
2858
|
+
if payload.get("query") != EXPECTED_CORPUS_SEARCH_QUERY:
|
|
2859
|
+
raise SystemExit(f"ranked search JSON after {context} query differs from expected query")
|
|
2860
|
+
if payload.get("ranked") is not True:
|
|
2861
|
+
raise SystemExit(f"ranked search JSON after {context} ranked flag is not true")
|
|
2862
|
+
notice = payload.get("notice")
|
|
2863
|
+
if not isinstance(notice, str):
|
|
2864
|
+
raise SystemExit(f"ranked search JSON after {context} notice is not a string")
|
|
2865
|
+
if expected_notice is not None and notice != expected_notice:
|
|
2866
|
+
raise SystemExit(f"ranked search JSON after {context} notice differs from expected notice")
|
|
2867
|
+
if expected_backend is not None and payload.get("backend") != expected_backend:
|
|
2868
|
+
raise SystemExit(f"ranked search JSON after {context} backend differs from expected backend")
|
|
2869
|
+
|
|
2870
|
+
hits = payload.get("hits")
|
|
2871
|
+
if not isinstance(hits, list):
|
|
2872
|
+
raise SystemExit(f"ranked search JSON after {context} hits is not a list")
|
|
2873
|
+
if not hits:
|
|
2874
|
+
raise SystemExit(f"ranked search JSON after {context} does not contain any hits")
|
|
2875
|
+
if len(hits) > EXPECTED_RANKED_SEARCH_LIMIT:
|
|
2876
|
+
raise SystemExit(f"ranked search JSON after {context} hit count exceeds the expected limit")
|
|
2877
|
+
|
|
2878
|
+
query_token = EXPECTED_CORPUS_SEARCH_QUERY.lower()
|
|
2879
|
+
query_token_matched = False
|
|
2880
|
+
for hit in hits:
|
|
2881
|
+
hit = assert_corpus_json_keys(
|
|
2882
|
+
hit,
|
|
2883
|
+
["relPath", "file", "score", "matchedTokens", "preview"],
|
|
2884
|
+
label="hit",
|
|
2885
|
+
context=context,
|
|
2886
|
+
command_label="ranked search JSON",
|
|
2887
|
+
)
|
|
2888
|
+
rel_path = hit.get("relPath")
|
|
2889
|
+
if not isinstance(rel_path, str) or not rel_path.startswith("knowledge/"):
|
|
2890
|
+
raise SystemExit(f"ranked search JSON after {context} hit relPath is outside the knowledge dir")
|
|
2891
|
+
assert_corpus_file_path(
|
|
2892
|
+
hit.get("file"),
|
|
2893
|
+
rel_path,
|
|
2894
|
+
label="hit",
|
|
2895
|
+
context=context,
|
|
2896
|
+
command_label="ranked search JSON",
|
|
2897
|
+
)
|
|
2898
|
+
if not is_ranked_search_positive_score(hit.get("score")):
|
|
2899
|
+
raise SystemExit(f"ranked search JSON after {context} hit score is not a positive number")
|
|
2900
|
+
matched_tokens = hit.get("matchedTokens")
|
|
2901
|
+
if not isinstance(matched_tokens, list) or not matched_tokens:
|
|
2902
|
+
raise SystemExit(f"ranked search JSON after {context} hit matchedTokens is empty")
|
|
2903
|
+
if not all(isinstance(token, str) and token for token in matched_tokens):
|
|
2904
|
+
raise SystemExit(f"ranked search JSON after {context} hit matchedTokens contains an invalid token")
|
|
2905
|
+
if not isinstance(hit.get("preview"), str) or not hit.get("preview"):
|
|
2906
|
+
raise SystemExit(f"ranked search JSON after {context} hit preview is empty")
|
|
2907
|
+
if query_token in matched_tokens:
|
|
2908
|
+
query_token_matched = True
|
|
2909
|
+
|
|
2910
|
+
if not query_token_matched:
|
|
2911
|
+
raise SystemExit(f"ranked search JSON after {context} is missing a hit matching the query token")
|
|
2912
|
+
|
|
2913
|
+
|
|
2914
|
+
def assert_ranked_search_determinism(
|
|
2915
|
+
first_raw: str,
|
|
2916
|
+
second_raw: str,
|
|
2917
|
+
*,
|
|
2918
|
+
context: str,
|
|
2919
|
+
cmd: list[str],
|
|
2920
|
+
) -> None:
|
|
2921
|
+
assert_no_ansi(first_raw, cmd)
|
|
2922
|
+
assert_no_ansi(second_raw, cmd)
|
|
2923
|
+
if first_raw != second_raw:
|
|
2924
|
+
raise SystemExit(f"ranked search JSON after {context} differs between identical runs")
|
|
2925
|
+
|
|
2926
|
+
|
|
2927
|
+
def assert_index_build_json(raw: str, *, index_dir: str, context: str, cmd: list[str]) -> None:
|
|
2928
|
+
assert_no_ansi(raw, cmd)
|
|
2929
|
+
try:
|
|
2930
|
+
payload = json.loads(raw)
|
|
2931
|
+
except json.JSONDecodeError as error:
|
|
2932
|
+
raise SystemExit(f"failed to parse index build JSON after {context}") from error
|
|
2933
|
+
|
|
2934
|
+
payload = assert_corpus_json_keys(
|
|
2935
|
+
payload,
|
|
2936
|
+
["action", "indexDir", "corpus", "learning"],
|
|
2937
|
+
label="top-level",
|
|
2938
|
+
context=context,
|
|
2939
|
+
command_label="index build JSON",
|
|
2940
|
+
)
|
|
2941
|
+
|
|
2942
|
+
if payload.get("action") != "build":
|
|
2943
|
+
raise SystemExit(f"index build JSON after {context} action differs from build")
|
|
2944
|
+
if payload.get("indexDir") != str(index_dir):
|
|
2945
|
+
raise SystemExit(f"index build JSON after {context} indexDir differs from expected directory")
|
|
2946
|
+
|
|
2947
|
+
for label in EXPECTED_INDEX_FILE_BASENAMES:
|
|
2948
|
+
section = assert_corpus_json_keys(
|
|
2949
|
+
payload.get(label),
|
|
2950
|
+
["file", "documentCount", "digest"],
|
|
2951
|
+
label=label,
|
|
2952
|
+
context=context,
|
|
2953
|
+
command_label="index build JSON",
|
|
2954
|
+
)
|
|
2955
|
+
assert_index_file_path(
|
|
2956
|
+
section.get("file"),
|
|
2957
|
+
index_dir=index_dir,
|
|
2958
|
+
label=label,
|
|
2959
|
+
context=context,
|
|
2960
|
+
command_label="index build JSON",
|
|
2961
|
+
)
|
|
2962
|
+
if not is_corpus_json_positive_int(section.get("documentCount")):
|
|
2963
|
+
raise SystemExit(f"index build JSON after {context} {label} documentCount is not a positive integer")
|
|
2964
|
+
if not is_index_sha256_digest(section.get("digest")):
|
|
2965
|
+
raise SystemExit(f"index build JSON after {context} {label} digest is not a sha256 digest")
|
|
2966
|
+
|
|
2967
|
+
|
|
2968
|
+
def assert_index_status_json(
|
|
2969
|
+
raw: str,
|
|
2970
|
+
*,
|
|
2971
|
+
index_dir: str,
|
|
2972
|
+
context: str,
|
|
2973
|
+
cmd: list[str],
|
|
2974
|
+
expect_embeddings: bool = False,
|
|
2975
|
+
) -> None:
|
|
2976
|
+
assert_no_ansi(raw, cmd)
|
|
2977
|
+
try:
|
|
2978
|
+
payload = json.loads(raw)
|
|
2979
|
+
except json.JSONDecodeError as error:
|
|
2980
|
+
raise SystemExit(f"failed to parse index status JSON after {context}") from error
|
|
2981
|
+
|
|
2982
|
+
payload = assert_corpus_json_keys(
|
|
2983
|
+
payload,
|
|
2984
|
+
["action", "indexDir", "corpus", "learning", "embeddings", "fresh", "buildCommand"],
|
|
2985
|
+
label="top-level",
|
|
2986
|
+
context=context,
|
|
2987
|
+
command_label="index status JSON",
|
|
2988
|
+
)
|
|
2989
|
+
|
|
2990
|
+
if payload.get("action") != "status":
|
|
2991
|
+
raise SystemExit(f"index status JSON after {context} action differs from status")
|
|
2992
|
+
if payload.get("indexDir") != str(index_dir):
|
|
2993
|
+
raise SystemExit(f"index status JSON after {context} indexDir differs from expected directory")
|
|
2994
|
+
if payload.get("fresh") is not True:
|
|
2995
|
+
raise SystemExit(f"index status JSON after {context} is not fresh")
|
|
2996
|
+
if payload.get("buildCommand") != EXPECTED_INDEX_BUILD_COMMAND:
|
|
2997
|
+
raise SystemExit(f"index status JSON after {context} buildCommand differs from expected command")
|
|
2998
|
+
|
|
2999
|
+
for label in EXPECTED_INDEX_FILE_BASENAMES:
|
|
3000
|
+
section = assert_corpus_json_keys(
|
|
3001
|
+
payload.get(label),
|
|
3002
|
+
EXPECTED_INDEX_STATUS_SECTION_KEYS,
|
|
3003
|
+
label=label,
|
|
3004
|
+
context=context,
|
|
3005
|
+
command_label="index status JSON",
|
|
3006
|
+
)
|
|
3007
|
+
assert_index_file_path(
|
|
3008
|
+
section.get("file"),
|
|
3009
|
+
index_dir=index_dir,
|
|
3010
|
+
label=label,
|
|
3011
|
+
context=context,
|
|
3012
|
+
command_label="index status JSON",
|
|
3013
|
+
)
|
|
3014
|
+
if section.get("present") is not True:
|
|
3015
|
+
raise SystemExit(f"index status JSON after {context} {label} index is not present")
|
|
3016
|
+
if section.get("fresh") is not True:
|
|
3017
|
+
raise SystemExit(f"index status JSON after {context} {label} index is not fresh")
|
|
3018
|
+
generated_at = section.get("generatedAt")
|
|
3019
|
+
if not isinstance(generated_at, str) or not generated_at:
|
|
3020
|
+
raise SystemExit(f"index status JSON after {context} {label} generatedAt is missing")
|
|
3021
|
+
if not is_corpus_json_positive_int(section.get("documentCount")):
|
|
3022
|
+
raise SystemExit(f"index status JSON after {context} {label} documentCount is not a positive integer")
|
|
3023
|
+
if not is_index_sha256_digest(section.get("storedDigest")):
|
|
3024
|
+
raise SystemExit(f"index status JSON after {context} {label} storedDigest is not a sha256 digest")
|
|
3025
|
+
if section.get("storedDigest") != section.get("currentDigest"):
|
|
3026
|
+
raise SystemExit(f"index status JSON after {context} {label} stored digest differs from current digest")
|
|
3027
|
+
if section.get("error") != "":
|
|
3028
|
+
raise SystemExit(f"index status JSON after {context} {label} error is not empty")
|
|
3029
|
+
|
|
3030
|
+
embeddings = payload.get("embeddings")
|
|
3031
|
+
if not expect_embeddings:
|
|
3032
|
+
if embeddings is not None:
|
|
3033
|
+
raise SystemExit(f"index status JSON after {context} embeddings is not null when embeddings were not built")
|
|
3034
|
+
return
|
|
3035
|
+
|
|
3036
|
+
embeddings = assert_corpus_json_keys(
|
|
3037
|
+
embeddings,
|
|
3038
|
+
EXPECTED_INDEX_STATUS_EMBEDDINGS_KEYS,
|
|
3039
|
+
label="embeddings",
|
|
3040
|
+
context=context,
|
|
3041
|
+
command_label="index status JSON",
|
|
3042
|
+
)
|
|
3043
|
+
expected_embeddings_file = str(Path(index_dir) / EXPECTED_EMBEDDING_INDEX_FILE_BASENAMES["embedding-index"])
|
|
3044
|
+
if embeddings.get("file") != expected_embeddings_file:
|
|
3045
|
+
raise SystemExit(f"index status JSON after {context} embeddings file differs from expected index path")
|
|
3046
|
+
if embeddings.get("present") is not True:
|
|
3047
|
+
raise SystemExit(f"index status JSON after {context} embeddings index is not present")
|
|
3048
|
+
if embeddings.get("fresh") is not True:
|
|
3049
|
+
raise SystemExit(f"index status JSON after {context} embeddings index is not fresh")
|
|
3050
|
+
if not is_corpus_json_positive_int(embeddings.get("documentCount")):
|
|
3051
|
+
raise SystemExit(f"index status JSON after {context} embeddings documentCount is not a positive integer")
|
|
3052
|
+
provider = embeddings.get("provider")
|
|
3053
|
+
if not isinstance(provider, dict) or not provider.get("command"):
|
|
3054
|
+
raise SystemExit(f"index status JSON after {context} embeddings provider is missing a command")
|
|
3055
|
+
if embeddings.get("error") != "":
|
|
3056
|
+
raise SystemExit(f"index status JSON after {context} embeddings error is not empty")
|
|
3057
|
+
|
|
3058
|
+
|
|
3059
|
+
def assert_index_verify_json(
|
|
3060
|
+
raw: str,
|
|
3061
|
+
*,
|
|
3062
|
+
index_dir: str,
|
|
3063
|
+
context: str,
|
|
3064
|
+
cmd: list[str],
|
|
3065
|
+
expect_embeddings_check: bool = False,
|
|
3066
|
+
) -> None:
|
|
3067
|
+
assert_no_ansi(raw, cmd)
|
|
3068
|
+
try:
|
|
3069
|
+
payload = json.loads(raw)
|
|
3070
|
+
except json.JSONDecodeError as error:
|
|
3071
|
+
raise SystemExit(f"failed to parse index verify JSON after {context}") from error
|
|
3072
|
+
|
|
3073
|
+
payload = assert_corpus_json_keys(
|
|
3074
|
+
payload,
|
|
3075
|
+
["action", "ok", "checks"],
|
|
3076
|
+
label="top-level",
|
|
3077
|
+
context=context,
|
|
3078
|
+
command_label="index verify JSON",
|
|
3079
|
+
)
|
|
3080
|
+
|
|
3081
|
+
if payload.get("action") != "verify":
|
|
3082
|
+
raise SystemExit(f"index verify JSON after {context} action differs from verify")
|
|
3083
|
+
if payload.get("ok") is not True:
|
|
3084
|
+
raise SystemExit(f"index verify JSON after {context} ok is not true")
|
|
3085
|
+
|
|
3086
|
+
checks = payload.get("checks")
|
|
3087
|
+
if not isinstance(checks, list):
|
|
3088
|
+
raise SystemExit(f"index verify JSON after {context} checks is not a list")
|
|
3089
|
+
expected_names = list(EXPECTED_INDEX_FILE_BASENAMES) + (["embeddings"] if expect_embeddings_check else [])
|
|
3090
|
+
if [check.get("name") for check in checks if isinstance(check, dict)] != expected_names:
|
|
3091
|
+
raise SystemExit(f"index verify JSON after {context} check names changed")
|
|
3092
|
+
|
|
3093
|
+
for check in checks:
|
|
3094
|
+
name = check.get("name") if isinstance(check, dict) else None
|
|
3095
|
+
if name == "embeddings":
|
|
3096
|
+
if check.get("matches") is not True:
|
|
3097
|
+
raise SystemExit(f"index verify JSON after {context} embeddings check does not match")
|
|
3098
|
+
if not isinstance(check.get("file"), str) or not check.get("file"):
|
|
3099
|
+
raise SystemExit(f"index verify JSON after {context} embeddings check file is missing")
|
|
3100
|
+
if not isinstance(check.get("reason"), str):
|
|
3101
|
+
raise SystemExit(f"index verify JSON after {context} embeddings check reason is not a string")
|
|
3102
|
+
continue
|
|
3103
|
+
|
|
3104
|
+
check = assert_corpus_json_keys(
|
|
3105
|
+
check,
|
|
3106
|
+
["name", "file", "matches", "reason"],
|
|
3107
|
+
label="check",
|
|
3108
|
+
context=context,
|
|
3109
|
+
command_label="index verify JSON",
|
|
3110
|
+
)
|
|
3111
|
+
assert_index_file_path(
|
|
3112
|
+
check.get("file"),
|
|
3113
|
+
index_dir=index_dir,
|
|
3114
|
+
label=name,
|
|
3115
|
+
context=context,
|
|
3116
|
+
command_label="index verify JSON",
|
|
3117
|
+
)
|
|
3118
|
+
if check.get("matches") is not True:
|
|
3119
|
+
raise SystemExit(f"index verify JSON after {context} {name} check does not match")
|
|
3120
|
+
if check.get("reason") != "":
|
|
3121
|
+
raise SystemExit(f"index verify JSON after {context} {name} check reason is not empty")
|
|
3122
|
+
|
|
3123
|
+
|
|
2670
3124
|
def assert_show_json_line(raw: str, *, context: str, cmd: list[str]) -> None:
|
|
2671
3125
|
assert_no_ansi(raw, cmd)
|
|
2672
3126
|
try:
|
|
@@ -3261,6 +3715,8 @@ def assert_route_explain_human_output(raw: str, *, context: str, cmd: list[str])
|
|
|
3261
3715
|
f"agent: ✓ {EXPECTED_ROUTE_AGENT}",
|
|
3262
3716
|
"read: ✓ knowledge/PRINCIPLES.md",
|
|
3263
3717
|
f"read: ✓ {EXPECTED_ROUTE_KNOWLEDGE}",
|
|
3718
|
+
# Advisory related-knowledge recall is surfaced only under --explain.
|
|
3719
|
+
"related: knowledge/",
|
|
3264
3720
|
),
|
|
3265
3721
|
context=context,
|
|
3266
3722
|
label="route explain human output",
|
|
@@ -4213,18 +4669,21 @@ def passing_main_help_output() -> str:
|
|
|
4213
4669
|
" design-ai help [command|--json]",
|
|
4214
4670
|
"",
|
|
4215
4671
|
" install Symlink design-ai into Claude Code (~/.claude)",
|
|
4216
|
-
" search <query> [--dir kind] [--limit N] [--
|
|
4672
|
+
" search <query> [--dir kind] [--limit N] [--ranked] [--embeddings [--provider \"cmd args\"]] [--json]",
|
|
4673
|
+
" Search the local markdown corpus",
|
|
4674
|
+
" index [--build|--status|--verify] [--json] [--embeddings [--provider \"cmd args\"]]",
|
|
4675
|
+
" Build, inspect, and verify the local retrieval index",
|
|
4217
4676
|
" show <file[:line]> [--lines N:M] [--context N] [--json] Print a corpus file or line range",
|
|
4218
4677
|
" route <brief|--from-file file|--stdin|--list|--eval-template|--eval> [--limit N]",
|
|
4219
4678
|
" Recommend commands, skills, knowledge, and route eval checkpoints",
|
|
4220
|
-
" prompt <brief|--from-file file|--stdin|--eval-template|--eval> [--route id] [--with-learning] [--learning-category kind] [--learning-limit N] [--out file]",
|
|
4679
|
+
" 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]",
|
|
4221
4680
|
" Generate a ready-to-use agent prompt and prompt-plan eval checkpoints",
|
|
4222
|
-
" pack <brief|--from-file file|--stdin|--eval-template|--eval> [--route id] [--with-learning] [--learning-category kind] [--learning-limit N] [--max-bytes N]",
|
|
4681
|
+
" 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]",
|
|
4223
4682
|
" Generate prompt plus bounded context and prompt-pack eval checkpoints",
|
|
4224
4683
|
" check <artifact.md|--stdin|--examples> [--route id|--all-routes] [--learn]",
|
|
4225
4684
|
" Check generated Markdown artifact quality; add --issues-only or --learn",
|
|
4226
4685
|
" examples [query] [--route id] [--limit N] [--json] Find worked examples for a route or query",
|
|
4227
|
-
" 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]",
|
|
4686
|
+
" 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]",
|
|
4228
4687
|
" Manage local learning preferences, usage reports, signal registry, agent backlog, skill proposals, and eval checkpoints for prompt personalization",
|
|
4229
4688
|
" workspace [--root path] [--learning-file path] [--learning-usage path] [--learning-eval path] [--strict] [--json]",
|
|
4230
4689
|
" Show read-only local dogfood readiness: git, repository, learning usage, eval checkpoints, and release scripts",
|
|
@@ -4326,6 +4785,7 @@ def passing_workspace_json() -> str:
|
|
|
4326
4785
|
"learningUsage": None,
|
|
4327
4786
|
"learningEval": None,
|
|
4328
4787
|
"learningRestoreBackups": None,
|
|
4788
|
+
"retrievalIndex": None,
|
|
4329
4789
|
"release": {
|
|
4330
4790
|
"packageName": "@design-ai/cli",
|
|
4331
4791
|
"version": EXPECTED_RELEASE_VERSION,
|
|
@@ -6409,6 +6869,44 @@ def assert_workspace_json(raw: str, *, context: str, cmd: list[str]) -> None:
|
|
|
6409
6869
|
if privacy.get("mutatesProfile") is not False:
|
|
6410
6870
|
raise SystemExit(f"workspace JSON after {context} learningRestoreBackups mutation privacy flag changed")
|
|
6411
6871
|
|
|
6872
|
+
retrieval_index = payload.get("retrievalIndex")
|
|
6873
|
+
if retrieval_index is not None:
|
|
6874
|
+
retrieval_index = assert_smoke_json_keys(
|
|
6875
|
+
retrieval_index,
|
|
6876
|
+
EXPECTED_WORKSPACE_RETRIEVAL_INDEX_KEYS,
|
|
6877
|
+
label="retrievalIndex",
|
|
6878
|
+
context=context,
|
|
6879
|
+
command_label="workspace JSON",
|
|
6880
|
+
)
|
|
6881
|
+
if not isinstance(retrieval_index.get("indexDir"), str) or not retrieval_index["indexDir"]:
|
|
6882
|
+
raise SystemExit(f"workspace JSON after {context} retrievalIndex indexDir is missing")
|
|
6883
|
+
if type(retrieval_index.get("fresh")) is not bool:
|
|
6884
|
+
raise SystemExit(f"workspace JSON after {context} retrievalIndex fresh is not boolean")
|
|
6885
|
+
if retrieval_index.get("status") not in ("pass", "warn"):
|
|
6886
|
+
raise SystemExit(f"workspace JSON after {context} retrievalIndex status is invalid")
|
|
6887
|
+
if (retrieval_index.get("status") == "pass") is not retrieval_index.get("fresh"):
|
|
6888
|
+
raise SystemExit(f"workspace JSON after {context} retrievalIndex status does not match freshness")
|
|
6889
|
+
if retrieval_index.get("buildCommand") != EXPECTED_INDEX_BUILD_COMMAND:
|
|
6890
|
+
raise SystemExit(f"workspace JSON after {context} retrievalIndex buildCommand differs from expected command")
|
|
6891
|
+
for section_label in EXPECTED_INDEX_FILE_BASENAMES:
|
|
6892
|
+
section = assert_smoke_json_keys(
|
|
6893
|
+
retrieval_index.get(section_label),
|
|
6894
|
+
EXPECTED_INDEX_STATUS_SECTION_KEYS,
|
|
6895
|
+
label=f"retrievalIndex {section_label}",
|
|
6896
|
+
context=context,
|
|
6897
|
+
command_label="workspace JSON",
|
|
6898
|
+
)
|
|
6899
|
+
if not isinstance(section.get("file"), str) or not section["file"]:
|
|
6900
|
+
raise SystemExit(f"workspace JSON after {context} retrievalIndex {section_label} file is missing")
|
|
6901
|
+
for key in ("present", "fresh"):
|
|
6902
|
+
if type(section.get(key)) is not bool:
|
|
6903
|
+
raise SystemExit(f"workspace JSON after {context} retrievalIndex {section_label} {key} is not boolean")
|
|
6904
|
+
if not is_lifecycle_json_non_negative_int(section.get("documentCount")):
|
|
6905
|
+
raise SystemExit(f"workspace JSON after {context} retrievalIndex {section_label} documentCount is invalid")
|
|
6906
|
+
for key in ("generatedAt", "storedDigest", "currentDigest", "error"):
|
|
6907
|
+
if not isinstance(section.get(key), str):
|
|
6908
|
+
raise SystemExit(f"workspace JSON after {context} retrievalIndex {section_label} {key} is not a string")
|
|
6909
|
+
|
|
6412
6910
|
release = assert_smoke_json_keys(
|
|
6413
6911
|
payload.get("release"),
|
|
6414
6912
|
EXPECTED_WORKSPACE_RELEASE_KEYS,
|
|
@@ -9149,6 +9647,364 @@ def run_self_test() -> None:
|
|
|
9149
9647
|
scope="smoke assertions",
|
|
9150
9648
|
)
|
|
9151
9649
|
|
|
9650
|
+
ranked_search_cmd = [
|
|
9651
|
+
"design-ai",
|
|
9652
|
+
"search",
|
|
9653
|
+
EXPECTED_CORPUS_SEARCH_QUERY,
|
|
9654
|
+
"--dir",
|
|
9655
|
+
"knowledge",
|
|
9656
|
+
"--limit",
|
|
9657
|
+
str(EXPECTED_RANKED_SEARCH_LIMIT),
|
|
9658
|
+
"--ranked",
|
|
9659
|
+
"--json",
|
|
9660
|
+
]
|
|
9661
|
+
assert_ranked_search_json(passing_ranked_search_json(), context=context, cmd=ranked_search_cmd)
|
|
9662
|
+
assert_ranked_search_json(
|
|
9663
|
+
passing_ranked_search_json(),
|
|
9664
|
+
context=context,
|
|
9665
|
+
cmd=ranked_search_cmd,
|
|
9666
|
+
expected_notice=EXPECTED_RANKED_SEARCH_NOT_BUILT_NOTICE,
|
|
9667
|
+
)
|
|
9668
|
+
expect_self_test_failure(
|
|
9669
|
+
lambda: assert_ranked_search_json("{", context=context, cmd=ranked_search_cmd),
|
|
9670
|
+
expected="failed to parse ranked search JSON",
|
|
9671
|
+
scope="smoke assertions",
|
|
9672
|
+
)
|
|
9673
|
+
assert_ranked_search_json(
|
|
9674
|
+
passing_ranked_search_json(backend="embeddings"),
|
|
9675
|
+
context=context,
|
|
9676
|
+
cmd=ranked_search_cmd,
|
|
9677
|
+
expected_backend="embeddings",
|
|
9678
|
+
)
|
|
9679
|
+
ranked_wrong_backend = json.loads(passing_ranked_search_json(backend="embeddings"))
|
|
9680
|
+
ranked_wrong_backend["backend"] = "lexical"
|
|
9681
|
+
expect_self_test_failure(
|
|
9682
|
+
lambda: assert_ranked_search_json(
|
|
9683
|
+
json.dumps(ranked_wrong_backend), context=context, cmd=ranked_search_cmd, expected_backend="embeddings"
|
|
9684
|
+
),
|
|
9685
|
+
expected="backend differs from expected backend",
|
|
9686
|
+
scope="smoke assertions",
|
|
9687
|
+
)
|
|
9688
|
+
ranked_wrong_query = json.loads(passing_ranked_search_json())
|
|
9689
|
+
ranked_wrong_query["query"] = "Inter"
|
|
9690
|
+
expect_self_test_failure(
|
|
9691
|
+
lambda: assert_ranked_search_json(json.dumps(ranked_wrong_query), context=context, cmd=ranked_search_cmd),
|
|
9692
|
+
expected="query differs",
|
|
9693
|
+
scope="smoke assertions",
|
|
9694
|
+
)
|
|
9695
|
+
ranked_flag_false = json.loads(passing_ranked_search_json())
|
|
9696
|
+
ranked_flag_false["ranked"] = False
|
|
9697
|
+
expect_self_test_failure(
|
|
9698
|
+
lambda: assert_ranked_search_json(json.dumps(ranked_flag_false), context=context, cmd=ranked_search_cmd),
|
|
9699
|
+
expected="ranked flag is not true",
|
|
9700
|
+
scope="smoke assertions",
|
|
9701
|
+
)
|
|
9702
|
+
ranked_wrong_notice = json.loads(passing_ranked_search_json())
|
|
9703
|
+
ranked_wrong_notice["notice"] = "corpus index is stale"
|
|
9704
|
+
expect_self_test_failure(
|
|
9705
|
+
lambda: assert_ranked_search_json(
|
|
9706
|
+
json.dumps(ranked_wrong_notice),
|
|
9707
|
+
context=context,
|
|
9708
|
+
cmd=ranked_search_cmd,
|
|
9709
|
+
expected_notice=EXPECTED_RANKED_SEARCH_NOT_BUILT_NOTICE,
|
|
9710
|
+
),
|
|
9711
|
+
expected="notice differs",
|
|
9712
|
+
scope="smoke assertions",
|
|
9713
|
+
)
|
|
9714
|
+
ranked_no_hits = json.loads(passing_ranked_search_json())
|
|
9715
|
+
ranked_no_hits["hits"] = []
|
|
9716
|
+
expect_self_test_failure(
|
|
9717
|
+
lambda: assert_ranked_search_json(json.dumps(ranked_no_hits), context=context, cmd=ranked_search_cmd),
|
|
9718
|
+
expected="does not contain any hits",
|
|
9719
|
+
scope="smoke assertions",
|
|
9720
|
+
)
|
|
9721
|
+
ranked_extra_hits = json.loads(passing_ranked_search_json())
|
|
9722
|
+
while len(ranked_extra_hits["hits"]) <= EXPECTED_RANKED_SEARCH_LIMIT:
|
|
9723
|
+
ranked_extra_hits["hits"].append(dict(ranked_extra_hits["hits"][0]))
|
|
9724
|
+
expect_self_test_failure(
|
|
9725
|
+
lambda: assert_ranked_search_json(json.dumps(ranked_extra_hits), context=context, cmd=ranked_search_cmd),
|
|
9726
|
+
expected="hit count exceeds",
|
|
9727
|
+
scope="smoke assertions",
|
|
9728
|
+
)
|
|
9729
|
+
ranked_zero_score = json.loads(passing_ranked_search_json())
|
|
9730
|
+
ranked_zero_score["hits"][0]["score"] = 0
|
|
9731
|
+
expect_self_test_failure(
|
|
9732
|
+
lambda: assert_ranked_search_json(json.dumps(ranked_zero_score), context=context, cmd=ranked_search_cmd),
|
|
9733
|
+
expected="score is not a positive number",
|
|
9734
|
+
scope="smoke assertions",
|
|
9735
|
+
)
|
|
9736
|
+
ranked_bool_score = json.loads(passing_ranked_search_json())
|
|
9737
|
+
ranked_bool_score["hits"][0]["score"] = True
|
|
9738
|
+
expect_self_test_failure(
|
|
9739
|
+
lambda: assert_ranked_search_json(json.dumps(ranked_bool_score), context=context, cmd=ranked_search_cmd),
|
|
9740
|
+
expected="score is not a positive number",
|
|
9741
|
+
scope="smoke assertions",
|
|
9742
|
+
)
|
|
9743
|
+
ranked_empty_tokens = json.loads(passing_ranked_search_json())
|
|
9744
|
+
ranked_empty_tokens["hits"][0]["matchedTokens"] = []
|
|
9745
|
+
expect_self_test_failure(
|
|
9746
|
+
lambda: assert_ranked_search_json(json.dumps(ranked_empty_tokens), context=context, cmd=ranked_search_cmd),
|
|
9747
|
+
expected="matchedTokens is empty",
|
|
9748
|
+
scope="smoke assertions",
|
|
9749
|
+
)
|
|
9750
|
+
ranked_unmatched_tokens = json.loads(passing_ranked_search_json())
|
|
9751
|
+
ranked_unmatched_tokens["hits"][0]["matchedTokens"] = ["inter"]
|
|
9752
|
+
expect_self_test_failure(
|
|
9753
|
+
lambda: assert_ranked_search_json(json.dumps(ranked_unmatched_tokens), context=context, cmd=ranked_search_cmd),
|
|
9754
|
+
expected="missing a hit matching the query token",
|
|
9755
|
+
scope="smoke assertions",
|
|
9756
|
+
)
|
|
9757
|
+
ranked_missing_hit_key = json.loads(passing_ranked_search_json())
|
|
9758
|
+
del ranked_missing_hit_key["hits"][0]["preview"]
|
|
9759
|
+
expect_self_test_failure(
|
|
9760
|
+
lambda: assert_ranked_search_json(json.dumps(ranked_missing_hit_key), context=context, cmd=ranked_search_cmd),
|
|
9761
|
+
expected="hit keys changed",
|
|
9762
|
+
scope="smoke assertions",
|
|
9763
|
+
)
|
|
9764
|
+
ranked_relative_file = json.loads(passing_ranked_search_json())
|
|
9765
|
+
ranked_relative_file["hits"][0]["file"] = EXPECTED_CORPUS_SEARCH_HIT
|
|
9766
|
+
expect_self_test_failure(
|
|
9767
|
+
lambda: assert_ranked_search_json(json.dumps(ranked_relative_file), context=context, cmd=ranked_search_cmd),
|
|
9768
|
+
expected="file is not absolute",
|
|
9769
|
+
scope="smoke assertions",
|
|
9770
|
+
)
|
|
9771
|
+
expect_self_test_failure(
|
|
9772
|
+
lambda: assert_ranked_search_json("\x1b[31m{}", context=context, cmd=ranked_search_cmd),
|
|
9773
|
+
expected="ANSI escape",
|
|
9774
|
+
scope="smoke assertions",
|
|
9775
|
+
)
|
|
9776
|
+
assert_ranked_search_determinism(
|
|
9777
|
+
passing_ranked_search_json(),
|
|
9778
|
+
passing_ranked_search_json(),
|
|
9779
|
+
context=context,
|
|
9780
|
+
cmd=ranked_search_cmd,
|
|
9781
|
+
)
|
|
9782
|
+
expect_self_test_failure(
|
|
9783
|
+
lambda: assert_ranked_search_determinism(
|
|
9784
|
+
passing_ranked_search_json(),
|
|
9785
|
+
passing_ranked_search_json() + "\n",
|
|
9786
|
+
context=context,
|
|
9787
|
+
cmd=ranked_search_cmd,
|
|
9788
|
+
),
|
|
9789
|
+
expected="differs between identical runs",
|
|
9790
|
+
scope="smoke assertions",
|
|
9791
|
+
)
|
|
9792
|
+
expect_self_test_failure(
|
|
9793
|
+
lambda: assert_ranked_search_determinism(
|
|
9794
|
+
"\x1b[31m{}",
|
|
9795
|
+
"\x1b[31m{}",
|
|
9796
|
+
context=context,
|
|
9797
|
+
cmd=ranked_search_cmd,
|
|
9798
|
+
),
|
|
9799
|
+
expected="ANSI escape",
|
|
9800
|
+
scope="smoke assertions",
|
|
9801
|
+
)
|
|
9802
|
+
|
|
9803
|
+
index_dir = "/tmp/design-ai-index"
|
|
9804
|
+
index_build_cmd = ["design-ai", "index", "--build", "--json"]
|
|
9805
|
+
assert_index_build_json(passing_index_build_json(index_dir), index_dir=index_dir, context=context, cmd=index_build_cmd)
|
|
9806
|
+
expect_self_test_failure(
|
|
9807
|
+
lambda: assert_index_build_json("{", index_dir=index_dir, context=context, cmd=index_build_cmd),
|
|
9808
|
+
expected="failed to parse index build JSON",
|
|
9809
|
+
scope="smoke assertions",
|
|
9810
|
+
)
|
|
9811
|
+
index_build_wrong_action = json.loads(passing_index_build_json(index_dir))
|
|
9812
|
+
index_build_wrong_action["action"] = "status"
|
|
9813
|
+
expect_self_test_failure(
|
|
9814
|
+
lambda: assert_index_build_json(
|
|
9815
|
+
json.dumps(index_build_wrong_action), index_dir=index_dir, context=context, cmd=index_build_cmd
|
|
9816
|
+
),
|
|
9817
|
+
expected="action differs from build",
|
|
9818
|
+
scope="smoke assertions",
|
|
9819
|
+
)
|
|
9820
|
+
expect_self_test_failure(
|
|
9821
|
+
lambda: assert_index_build_json(
|
|
9822
|
+
passing_index_build_json("/tmp/other-index"), index_dir=index_dir, context=context, cmd=index_build_cmd
|
|
9823
|
+
),
|
|
9824
|
+
expected="indexDir differs",
|
|
9825
|
+
scope="smoke assertions",
|
|
9826
|
+
)
|
|
9827
|
+
index_build_zero_documents = json.loads(passing_index_build_json(index_dir))
|
|
9828
|
+
index_build_zero_documents["corpus"]["documentCount"] = 0
|
|
9829
|
+
expect_self_test_failure(
|
|
9830
|
+
lambda: assert_index_build_json(
|
|
9831
|
+
json.dumps(index_build_zero_documents), index_dir=index_dir, context=context, cmd=index_build_cmd
|
|
9832
|
+
),
|
|
9833
|
+
expected="documentCount is not a positive integer",
|
|
9834
|
+
scope="smoke assertions",
|
|
9835
|
+
)
|
|
9836
|
+
index_build_bad_digest = json.loads(passing_index_build_json(index_dir))
|
|
9837
|
+
index_build_bad_digest["learning"]["digest"] = "md5:abc"
|
|
9838
|
+
expect_self_test_failure(
|
|
9839
|
+
lambda: assert_index_build_json(
|
|
9840
|
+
json.dumps(index_build_bad_digest), index_dir=index_dir, context=context, cmd=index_build_cmd
|
|
9841
|
+
),
|
|
9842
|
+
expected="digest is not a sha256 digest",
|
|
9843
|
+
scope="smoke assertions",
|
|
9844
|
+
)
|
|
9845
|
+
index_build_missing_key = json.loads(passing_index_build_json(index_dir))
|
|
9846
|
+
del index_build_missing_key["corpus"]["file"]
|
|
9847
|
+
expect_self_test_failure(
|
|
9848
|
+
lambda: assert_index_build_json(
|
|
9849
|
+
json.dumps(index_build_missing_key), index_dir=index_dir, context=context, cmd=index_build_cmd
|
|
9850
|
+
),
|
|
9851
|
+
expected="corpus keys changed",
|
|
9852
|
+
scope="smoke assertions",
|
|
9853
|
+
)
|
|
9854
|
+
expect_self_test_failure(
|
|
9855
|
+
lambda: assert_index_build_json("\x1b[31m{}", index_dir=index_dir, context=context, cmd=index_build_cmd),
|
|
9856
|
+
expected="ANSI escape",
|
|
9857
|
+
scope="smoke assertions",
|
|
9858
|
+
)
|
|
9859
|
+
|
|
9860
|
+
index_status_cmd = ["design-ai", "index", "--status", "--json"]
|
|
9861
|
+
assert_index_status_json(passing_index_status_json(index_dir), index_dir=index_dir, context=context, cmd=index_status_cmd)
|
|
9862
|
+
expect_self_test_failure(
|
|
9863
|
+
lambda: assert_index_status_json("{", index_dir=index_dir, context=context, cmd=index_status_cmd),
|
|
9864
|
+
expected="failed to parse index status JSON",
|
|
9865
|
+
scope="smoke assertions",
|
|
9866
|
+
)
|
|
9867
|
+
index_status_stale = json.loads(passing_index_status_json(index_dir))
|
|
9868
|
+
index_status_stale["fresh"] = False
|
|
9869
|
+
expect_self_test_failure(
|
|
9870
|
+
lambda: assert_index_status_json(
|
|
9871
|
+
json.dumps(index_status_stale), index_dir=index_dir, context=context, cmd=index_status_cmd
|
|
9872
|
+
),
|
|
9873
|
+
expected="is not fresh",
|
|
9874
|
+
scope="smoke assertions",
|
|
9875
|
+
)
|
|
9876
|
+
index_status_wrong_command = json.loads(passing_index_status_json(index_dir))
|
|
9877
|
+
index_status_wrong_command["buildCommand"] = "design-ai rebuild"
|
|
9878
|
+
expect_self_test_failure(
|
|
9879
|
+
lambda: assert_index_status_json(
|
|
9880
|
+
json.dumps(index_status_wrong_command), index_dir=index_dir, context=context, cmd=index_status_cmd
|
|
9881
|
+
),
|
|
9882
|
+
expected="buildCommand differs",
|
|
9883
|
+
scope="smoke assertions",
|
|
9884
|
+
)
|
|
9885
|
+
index_status_missing_section = json.loads(passing_index_status_json(index_dir))
|
|
9886
|
+
index_status_missing_section["corpus"]["present"] = False
|
|
9887
|
+
expect_self_test_failure(
|
|
9888
|
+
lambda: assert_index_status_json(
|
|
9889
|
+
json.dumps(index_status_missing_section), index_dir=index_dir, context=context, cmd=index_status_cmd
|
|
9890
|
+
),
|
|
9891
|
+
expected="corpus index is not present",
|
|
9892
|
+
scope="smoke assertions",
|
|
9893
|
+
)
|
|
9894
|
+
index_status_stale_section = json.loads(passing_index_status_json(index_dir))
|
|
9895
|
+
index_status_stale_section["learning"]["fresh"] = False
|
|
9896
|
+
expect_self_test_failure(
|
|
9897
|
+
lambda: assert_index_status_json(
|
|
9898
|
+
json.dumps(index_status_stale_section), index_dir=index_dir, context=context, cmd=index_status_cmd
|
|
9899
|
+
),
|
|
9900
|
+
expected="learning index is not fresh",
|
|
9901
|
+
scope="smoke assertions",
|
|
9902
|
+
)
|
|
9903
|
+
index_status_digest_drift = json.loads(passing_index_status_json(index_dir))
|
|
9904
|
+
index_status_digest_drift["corpus"]["currentDigest"] = f"sha256:{'2' * 64}"
|
|
9905
|
+
expect_self_test_failure(
|
|
9906
|
+
lambda: assert_index_status_json(
|
|
9907
|
+
json.dumps(index_status_digest_drift), index_dir=index_dir, context=context, cmd=index_status_cmd
|
|
9908
|
+
),
|
|
9909
|
+
expected="stored digest differs from current digest",
|
|
9910
|
+
scope="smoke assertions",
|
|
9911
|
+
)
|
|
9912
|
+
index_status_error = json.loads(passing_index_status_json(index_dir))
|
|
9913
|
+
index_status_error["learning"]["error"] = "unreadable"
|
|
9914
|
+
expect_self_test_failure(
|
|
9915
|
+
lambda: assert_index_status_json(
|
|
9916
|
+
json.dumps(index_status_error), index_dir=index_dir, context=context, cmd=index_status_cmd
|
|
9917
|
+
),
|
|
9918
|
+
expected="error is not empty",
|
|
9919
|
+
scope="smoke assertions",
|
|
9920
|
+
)
|
|
9921
|
+
index_status_embeddings_present_unexpectedly = json.loads(passing_index_status_json(index_dir, with_embeddings=True))
|
|
9922
|
+
expect_self_test_failure(
|
|
9923
|
+
lambda: assert_index_status_json(
|
|
9924
|
+
json.dumps(index_status_embeddings_present_unexpectedly), index_dir=index_dir, context=context, cmd=index_status_cmd
|
|
9925
|
+
),
|
|
9926
|
+
expected="embeddings is not null when embeddings were not built",
|
|
9927
|
+
scope="smoke assertions",
|
|
9928
|
+
)
|
|
9929
|
+
|
|
9930
|
+
assert_index_status_json(
|
|
9931
|
+
passing_index_status_json(index_dir, with_embeddings=True),
|
|
9932
|
+
index_dir=index_dir,
|
|
9933
|
+
context=context,
|
|
9934
|
+
cmd=index_status_cmd,
|
|
9935
|
+
expect_embeddings=True,
|
|
9936
|
+
)
|
|
9937
|
+
index_status_embeddings_stale = json.loads(passing_index_status_json(index_dir, with_embeddings=True))
|
|
9938
|
+
index_status_embeddings_stale["embeddings"]["fresh"] = False
|
|
9939
|
+
expect_self_test_failure(
|
|
9940
|
+
lambda: assert_index_status_json(
|
|
9941
|
+
json.dumps(index_status_embeddings_stale),
|
|
9942
|
+
index_dir=index_dir,
|
|
9943
|
+
context=context,
|
|
9944
|
+
cmd=index_status_cmd,
|
|
9945
|
+
expect_embeddings=True,
|
|
9946
|
+
),
|
|
9947
|
+
expected="embeddings index is not fresh",
|
|
9948
|
+
scope="smoke assertions",
|
|
9949
|
+
)
|
|
9950
|
+
index_status_embeddings_no_provider_command = json.loads(passing_index_status_json(index_dir, with_embeddings=True))
|
|
9951
|
+
index_status_embeddings_no_provider_command["embeddings"]["provider"] = {"command": "", "modelLabel": "", "dimensions": 3}
|
|
9952
|
+
expect_self_test_failure(
|
|
9953
|
+
lambda: assert_index_status_json(
|
|
9954
|
+
json.dumps(index_status_embeddings_no_provider_command),
|
|
9955
|
+
index_dir=index_dir,
|
|
9956
|
+
context=context,
|
|
9957
|
+
cmd=index_status_cmd,
|
|
9958
|
+
expect_embeddings=True,
|
|
9959
|
+
),
|
|
9960
|
+
expected="embeddings provider is missing a command",
|
|
9961
|
+
scope="smoke assertions",
|
|
9962
|
+
)
|
|
9963
|
+
|
|
9964
|
+
index_verify_cmd = ["design-ai", "index", "--verify", "--json"]
|
|
9965
|
+
assert_index_verify_json(passing_index_verify_json(index_dir), index_dir=index_dir, context=context, cmd=index_verify_cmd)
|
|
9966
|
+
expect_self_test_failure(
|
|
9967
|
+
lambda: assert_index_verify_json("{", index_dir=index_dir, context=context, cmd=index_verify_cmd),
|
|
9968
|
+
expected="failed to parse index verify JSON",
|
|
9969
|
+
scope="smoke assertions",
|
|
9970
|
+
)
|
|
9971
|
+
index_verify_not_ok = json.loads(passing_index_verify_json(index_dir))
|
|
9972
|
+
index_verify_not_ok["ok"] = False
|
|
9973
|
+
expect_self_test_failure(
|
|
9974
|
+
lambda: assert_index_verify_json(
|
|
9975
|
+
json.dumps(index_verify_not_ok), index_dir=index_dir, context=context, cmd=index_verify_cmd
|
|
9976
|
+
),
|
|
9977
|
+
expected="ok is not true",
|
|
9978
|
+
scope="smoke assertions",
|
|
9979
|
+
)
|
|
9980
|
+
index_verify_wrong_names = json.loads(passing_index_verify_json(index_dir))
|
|
9981
|
+
index_verify_wrong_names["checks"] = index_verify_wrong_names["checks"][:1]
|
|
9982
|
+
expect_self_test_failure(
|
|
9983
|
+
lambda: assert_index_verify_json(
|
|
9984
|
+
json.dumps(index_verify_wrong_names), index_dir=index_dir, context=context, cmd=index_verify_cmd
|
|
9985
|
+
),
|
|
9986
|
+
expected="check names changed",
|
|
9987
|
+
scope="smoke assertions",
|
|
9988
|
+
)
|
|
9989
|
+
index_verify_mismatch = json.loads(passing_index_verify_json(index_dir))
|
|
9990
|
+
index_verify_mismatch["checks"][1]["matches"] = False
|
|
9991
|
+
expect_self_test_failure(
|
|
9992
|
+
lambda: assert_index_verify_json(
|
|
9993
|
+
json.dumps(index_verify_mismatch), index_dir=index_dir, context=context, cmd=index_verify_cmd
|
|
9994
|
+
),
|
|
9995
|
+
expected="learning check does not match",
|
|
9996
|
+
scope="smoke assertions",
|
|
9997
|
+
)
|
|
9998
|
+
index_verify_reason = json.loads(passing_index_verify_json(index_dir))
|
|
9999
|
+
index_verify_reason["checks"][0]["reason"] = "digest drift"
|
|
10000
|
+
expect_self_test_failure(
|
|
10001
|
+
lambda: assert_index_verify_json(
|
|
10002
|
+
json.dumps(index_verify_reason), index_dir=index_dir, context=context, cmd=index_verify_cmd
|
|
10003
|
+
),
|
|
10004
|
+
expected="corpus check reason is not empty",
|
|
10005
|
+
scope="smoke assertions",
|
|
10006
|
+
)
|
|
10007
|
+
|
|
9152
10008
|
show_cmd = ["design-ai", "show", EXPECTED_CORPUS_SHOW_TARGET, "--context", "0", "--json"]
|
|
9153
10009
|
assert_show_json_line(passing_show_json(), context=context, cmd=show_cmd)
|
|
9154
10010
|
expect_self_test_failure(
|
|
@@ -11012,6 +11868,54 @@ def run_self_test() -> None:
|
|
|
11012
11868
|
expected="privacy flags changed",
|
|
11013
11869
|
scope="smoke assertions",
|
|
11014
11870
|
)
|
|
11871
|
+
workspace_retrieval_index_payload = json.loads(passing_workspace_json())
|
|
11872
|
+
workspace_retrieval_index_payload["retrievalIndex"] = {
|
|
11873
|
+
"indexDir": "/tmp/design-ai-index",
|
|
11874
|
+
"corpus": json.loads(passing_index_status_json())["corpus"],
|
|
11875
|
+
"learning": json.loads(passing_index_status_json())["learning"],
|
|
11876
|
+
"fresh": True,
|
|
11877
|
+
"status": "pass",
|
|
11878
|
+
"buildCommand": EXPECTED_INDEX_BUILD_COMMAND,
|
|
11879
|
+
}
|
|
11880
|
+
assert_workspace_json(json.dumps(workspace_retrieval_index_payload), context=context, cmd=workspace_cmd)
|
|
11881
|
+
stale_retrieval_index_payload = json.loads(json.dumps(workspace_retrieval_index_payload))
|
|
11882
|
+
stale_retrieval_index_payload["retrievalIndex"]["fresh"] = False
|
|
11883
|
+
stale_retrieval_index_payload["retrievalIndex"]["status"] = "warn"
|
|
11884
|
+
stale_retrieval_index_payload["retrievalIndex"]["corpus"]["fresh"] = False
|
|
11885
|
+
assert_workspace_json(json.dumps(stale_retrieval_index_payload), context=context, cmd=workspace_cmd)
|
|
11886
|
+
mismatched_retrieval_index_payload = json.loads(json.dumps(workspace_retrieval_index_payload))
|
|
11887
|
+
mismatched_retrieval_index_payload["retrievalIndex"]["status"] = "warn"
|
|
11888
|
+
expect_self_test_failure(
|
|
11889
|
+
lambda: assert_workspace_json(
|
|
11890
|
+
json.dumps(mismatched_retrieval_index_payload),
|
|
11891
|
+
context=context,
|
|
11892
|
+
cmd=workspace_cmd,
|
|
11893
|
+
),
|
|
11894
|
+
expected="retrievalIndex status does not match freshness",
|
|
11895
|
+
scope="smoke assertions",
|
|
11896
|
+
)
|
|
11897
|
+
wrong_build_command_retrieval_index_payload = json.loads(json.dumps(workspace_retrieval_index_payload))
|
|
11898
|
+
wrong_build_command_retrieval_index_payload["retrievalIndex"]["buildCommand"] = "design-ai rebuild"
|
|
11899
|
+
expect_self_test_failure(
|
|
11900
|
+
lambda: assert_workspace_json(
|
|
11901
|
+
json.dumps(wrong_build_command_retrieval_index_payload),
|
|
11902
|
+
context=context,
|
|
11903
|
+
cmd=workspace_cmd,
|
|
11904
|
+
),
|
|
11905
|
+
expected="retrievalIndex buildCommand differs",
|
|
11906
|
+
scope="smoke assertions",
|
|
11907
|
+
)
|
|
11908
|
+
missing_retrieval_index_key_payload = json.loads(json.dumps(workspace_retrieval_index_payload))
|
|
11909
|
+
del missing_retrieval_index_key_payload["retrievalIndex"]["learning"]["documentCount"]
|
|
11910
|
+
expect_self_test_failure(
|
|
11911
|
+
lambda: assert_workspace_json(
|
|
11912
|
+
json.dumps(missing_retrieval_index_key_payload),
|
|
11913
|
+
context=context,
|
|
11914
|
+
cmd=workspace_cmd,
|
|
11915
|
+
),
|
|
11916
|
+
expected="retrievalIndex learning keys changed",
|
|
11917
|
+
scope="smoke assertions",
|
|
11918
|
+
)
|
|
11015
11919
|
assert_workspace_strict_failure_json(
|
|
11016
11920
|
passing_workspace_json(),
|
|
11017
11921
|
returncode=1,
|
|
@@ -11068,6 +11972,7 @@ def run_self_test() -> None:
|
|
|
11068
11972
|
"learningUsage": reordered_workspace_payload["learningUsage"],
|
|
11069
11973
|
"learningEval": reordered_workspace_payload["learningEval"],
|
|
11070
11974
|
"learningRestoreBackups": reordered_workspace_payload["learningRestoreBackups"],
|
|
11975
|
+
"retrievalIndex": reordered_workspace_payload["retrievalIndex"],
|
|
11071
11976
|
"release": reordered_workspace_payload["release"],
|
|
11072
11977
|
"nextActions": reordered_workspace_payload["nextActions"],
|
|
11073
11978
|
}
|