@design-ai/cli 4.56.0 → 4.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +109 -25
- package/CHANGELOG.md +36 -0
- package/README.ko.md +9 -26
- package/README.md +10 -23
- package/cli/bin/design-ai.mjs +1 -0
- package/cli/commands/help.mjs +7 -4
- package/cli/commands/index.mjs +292 -0
- package/cli/commands/learn-help.mjs +149 -0
- package/cli/commands/learn-print-profile.mjs +373 -0
- package/cli/commands/learn-print-restore.mjs +349 -0
- package/cli/commands/learn-print-signals.mjs +453 -0
- package/cli/commands/learn.mjs +60 -1281
- package/cli/commands/pack.mjs +7 -3
- package/cli/commands/prompt.mjs +7 -3
- package/cli/commands/search.mjs +94 -1
- package/cli/lib/dispatch.mjs +3 -0
- package/cli/lib/embedding-index.mjs +199 -0
- package/cli/lib/embedding-provider.mjs +121 -0
- package/cli/lib/embedding-rerank.mjs +52 -0
- package/cli/lib/learn-args.mjs +490 -0
- package/cli/lib/learn-backup.mjs +748 -0
- package/cli/lib/learn-curation.mjs +612 -0
- package/cli/lib/learn-eval.mjs +459 -0
- package/cli/lib/learn-profile.mjs +763 -0
- package/cli/lib/learn-select.mjs +223 -0
- package/cli/lib/learn-shared.mjs +158 -0
- package/cli/lib/learn-test-support.mjs +218 -0
- package/cli/lib/learn-usage.mjs +360 -0
- package/cli/lib/learn.mjs +83 -3694
- package/cli/lib/lexical.mjs +137 -0
- package/cli/lib/local-config.mjs +110 -0
- package/cli/lib/mcp-server.mjs +13 -3
- package/cli/lib/pack.mjs +31 -4
- package/cli/lib/prompt.mjs +42 -4
- package/cli/lib/recall.mjs +193 -0
- package/cli/lib/retrieval-index.mjs +0 -0
- package/cli/lib/search-ranked.mjs +170 -0
- package/cli/lib/search.mjs +11 -1
- package/cli/lib/signals-backlog-commands.mjs +673 -0
- package/cli/lib/signals-backlog.mjs +361 -0
- package/cli/lib/signals-eval.mjs +176 -0
- package/cli/lib/signals-readiness.mjs +247 -0
- package/cli/lib/signals-registry.mjs +377 -0
- package/cli/lib/signals-render.mjs +478 -0
- package/cli/lib/signals-shared.mjs +75 -0
- package/cli/lib/signals.mjs +16 -2337
- package/cli/lib/site-bundle-handoff-expected.mjs +173 -0
- package/cli/lib/site-bundle-handoff-runbook-action-summary.mjs +332 -0
- package/cli/lib/site-bundle-handoff-runbook-human-lines.mjs +167 -0
- package/cli/lib/site-bundle-handoff-runbook-maps.mjs +238 -0
- package/cli/lib/site-bundle-handoff-runbook-next-step.mjs +278 -0
- package/cli/lib/site-bundle-handoff-runbook.mjs +44 -931
- package/cli/lib/site-test-support.mjs +68 -0
- package/cli/lib/skill-proposals-apply-commands.mjs +135 -0
- package/cli/lib/skill-proposals-apply-contract.mjs +750 -0
- package/cli/lib/skill-proposals-apply-plan.mjs +118 -0
- package/cli/lib/skill-proposals-generate.mjs +298 -0
- package/cli/lib/skill-proposals-render.mjs +532 -0
- package/cli/lib/skill-proposals-review.mjs +262 -0
- package/cli/lib/skill-proposals.mjs +15 -2046
- package/cli/lib/workspace-args.mjs +83 -0
- package/cli/lib/workspace-git.mjs +169 -0
- package/cli/lib/workspace-learning.mjs +483 -0
- package/cli/lib/workspace-repo.mjs +139 -0
- package/cli/lib/workspace-report.mjs +283 -0
- package/cli/lib/workspace-test-support.mjs +99 -0
- package/cli/lib/workspace.mjs +30 -1056
- package/docs/AI-LEARNING-PHASE2.md +215 -0
- package/docs/DISTRIBUTION.ko.md +2 -2
- package/docs/DISTRIBUTION.md +2 -2
- package/docs/NEXT-SURFACE-DECISION.md +125 -0
- package/docs/PRODUCT-READINESS.md +3 -3
- package/docs/RELEASE-GATES.ko.md +35 -0
- package/docs/RELEASE-GATES.md +234 -0
- package/docs/ROADMAP.md +53 -8
- package/docs/external-status.md +6 -8
- package/docs/inspection-20260630.md +169 -0
- package/docs/integrations/design-ai-mcp-server.md +4 -4
- package/docs/reference/ant-design.md +413 -0
- package/docs/reference/awesome-design-md.md +439 -0
- package/docs/reference/mui.md +783 -0
- package/docs/reference/shadcn-ui.md +298 -0
- package/examples/component-accordion-actions.md +5 -5
- package/examples/component-accordion-details.md +5 -5
- package/examples/component-accordion-summary.md +5 -5
- package/examples/component-accordion.md +3 -3
- package/examples/component-affix.md +1 -1
- package/examples/component-alert-dialog.md +1 -1
- package/examples/component-alert-title.md +1 -1
- package/examples/component-alert.md +3 -3
- package/examples/component-anchor.md +1 -1
- package/examples/component-app-bar.md +1 -1
- package/examples/component-aspect-ratio.md +1 -1
- package/examples/component-auto-complete.md +2 -2
- package/examples/component-avatar-group.md +5 -5
- package/examples/component-avatar.md +3 -3
- package/examples/component-back-top.md +1 -1
- package/examples/component-backdrop.md +1 -1
- package/examples/component-badge.md +3 -3
- package/examples/component-border-beam.md +3 -3
- package/examples/component-bottom-navigation.md +1 -1
- package/examples/component-box.md +1 -1
- package/examples/component-breadcrumb.md +3 -3
- package/examples/component-button-base.md +3 -3
- package/examples/component-button-group.md +2 -2
- package/examples/component-button.md +3 -3
- package/examples/component-calendar.md +2 -2
- package/examples/component-card-actions.md +1 -1
- package/examples/component-card-content.md +1 -1
- package/examples/component-card-header.md +1 -1
- package/examples/component-card-media.md +1 -1
- package/examples/component-card.md +3 -3
- package/examples/component-carousel.md +2 -2
- package/examples/component-cascader.md +1 -1
- package/examples/component-chart.md +1 -1
- package/examples/component-checkbox.md +3 -3
- package/examples/component-click-away-listener.md +1 -1
- package/examples/component-code.md +1 -1
- package/examples/component-collapsible.md +1 -1
- package/examples/component-color-picker.md +1 -1
- package/examples/component-combobox.md +1 -1
- package/examples/component-command.md +1 -1
- package/examples/component-config-provider.md +3 -3
- package/examples/component-context-menu.md +1 -1
- package/examples/component-css-baseline.md +3 -3
- package/examples/component-date-picker.md +2 -2
- package/examples/component-descriptions.md +1 -1
- package/examples/component-dialog-actions.md +1 -1
- package/examples/component-dialog-content-text.md +1 -1
- package/examples/component-dialog-content.md +1 -1
- package/examples/component-dialog-title.md +1 -1
- package/examples/component-dialog.md +2 -2
- package/examples/component-divider.md +3 -3
- package/examples/component-drawer.md +3 -3
- package/examples/component-dropdown.md +3 -3
- package/examples/component-empty.md +2 -2
- package/examples/component-fade.md +1 -1
- package/examples/component-field.md +1 -1
- package/examples/component-filled-input.md +1 -1
- package/examples/component-flex.md +1 -1
- package/examples/component-float-button.md +2 -2
- package/examples/component-form-control-label.md +1 -1
- package/examples/component-form-control.md +1 -1
- package/examples/component-form-controls.md +1 -1
- package/examples/component-form-group.md +1 -1
- package/examples/component-form-helper-text.md +1 -1
- package/examples/component-form-label.md +1 -1
- package/examples/component-form.md +2 -2
- package/examples/component-grid.md +2 -2
- package/examples/component-grow.md +1 -1
- package/examples/component-hover-card.md +1 -1
- package/examples/component-icon-button.md +1 -1
- package/examples/component-icon.md +1 -1
- package/examples/component-image-list.md +1 -1
- package/examples/component-image.md +1 -1
- package/examples/component-input-adornment.md +1 -1
- package/examples/component-input-base.md +1 -1
- package/examples/component-input-number.md +1 -1
- package/examples/component-input-otp.md +2 -2
- package/examples/component-input.md +3 -3
- package/examples/component-item.md +1 -1
- package/examples/component-label.md +1 -1
- package/examples/component-layout.md +1 -1
- package/examples/component-link.md +1 -1
- package/examples/component-list-item-avatar.md +1 -1
- package/examples/component-list-item-button.md +1 -1
- package/examples/component-list-item-icon.md +1 -1
- package/examples/component-list-item-text.md +1 -1
- package/examples/component-list-item.md +1 -1
- package/examples/component-list-subheader.md +1 -1
- package/examples/component-list.md +2 -2
- package/examples/component-masonry.md +1 -1
- package/examples/component-mentions.md +1 -1
- package/examples/component-menu-item.md +1 -1
- package/examples/component-menu-list.md +1 -1
- package/examples/component-menu.md +1 -1
- package/examples/component-menubar.md +1 -1
- package/examples/component-message.md +1 -1
- package/examples/component-mobile-stepper.md +1 -1
- package/examples/component-modal.md +5 -5
- package/examples/component-navigation-menu.md +1 -1
- package/examples/component-notification.md +1 -1
- package/examples/component-outlined-input.md +1 -1
- package/examples/component-pagination.md +3 -3
- package/examples/component-paper.md +1 -1
- package/examples/component-popconfirm.md +1 -1
- package/examples/component-popover.md +3 -3
- package/examples/component-popper.md +1 -1
- package/examples/component-progress.md +2 -2
- package/examples/component-qr-code.md +1 -1
- package/examples/component-radio.md +3 -3
- package/examples/component-rate.md +2 -2
- package/examples/component-resizable.md +1 -1
- package/examples/component-result.md +1 -1
- package/examples/component-scroll-area.md +1 -1
- package/examples/component-segmented.md +3 -3
- package/examples/component-select.md +3 -3
- package/examples/component-separator.md +1 -1
- package/examples/component-sheet.md +1 -1
- package/examples/component-sidebar.md +1 -1
- package/examples/component-skeleton.md +3 -3
- package/examples/component-slide.md +1 -1
- package/examples/component-slider.md +3 -3
- package/examples/component-snackbar-content.md +1 -1
- package/examples/component-snackbar.md +1 -1
- package/examples/component-sonner.md +1 -1
- package/examples/component-space.md +1 -1
- package/examples/component-speed-dial-action.md +1 -1
- package/examples/component-speed-dial.md +1 -1
- package/examples/component-spin.md +2 -2
- package/examples/component-spinner.md +1 -1
- package/examples/component-splitter.md +2 -2
- package/examples/component-stack.md +1 -1
- package/examples/component-statistic.md +1 -1
- package/examples/component-step-button.md +4 -4
- package/examples/component-step-connector.md +4 -4
- package/examples/component-step-content.md +1 -1
- package/examples/component-step-icon.md +1 -1
- package/examples/component-step-label.md +1 -1
- package/examples/component-step.md +2 -2
- package/examples/component-steps.md +2 -2
- package/examples/component-swipeable-drawer.md +1 -1
- package/examples/component-switch.md +3 -3
- package/examples/component-tab-scroll-button.md +5 -5
- package/examples/component-tab.md +1 -1
- package/examples/component-table-body.md +1 -1
- package/examples/component-table-cell.md +1 -1
- package/examples/component-table-container.md +1 -1
- package/examples/component-table-footer.md +1 -1
- package/examples/component-table-head.md +1 -1
- package/examples/component-table-pagination.md +1 -1
- package/examples/component-table-row.md +1 -1
- package/examples/component-table-sort-label.md +1 -1
- package/examples/component-table.md +2 -2
- package/examples/component-tabs.md +3 -3
- package/examples/component-tag-badge.md +2 -2
- package/examples/component-tag.md +1 -1
- package/examples/component-textarea-autosize.md +1 -1
- package/examples/component-textarea.md +1 -1
- package/examples/component-time-picker.md +1 -1
- package/examples/component-timeline.md +1 -1
- package/examples/component-toast.md +4 -4
- package/examples/component-toggle-button.md +1 -1
- package/examples/component-toggle.md +1 -1
- package/examples/component-toolbar.md +1 -1
- package/examples/component-tooltip.md +3 -3
- package/examples/component-tour.md +1 -1
- package/examples/component-transfer.md +1 -1
- package/examples/component-tree-select.md +1 -1
- package/examples/component-tree.md +1 -1
- package/examples/component-typography.md +2 -2
- package/examples/component-upload.md +1 -1
- package/examples/component-watermark.md +1 -1
- package/examples/component-zoom.md +1 -1
- package/knowledge/COVERAGE.md +4 -3
- package/knowledge/components/INDEX.md +202 -202
- package/knowledge/patterns/brand-references.md +72 -72
- package/package.json +1 -1
- package/tools/audit/local-ci.py +16 -1
- package/tools/audit/package-smoke.py +611 -9
- package/tools/audit/registry-smoke.py +534 -0
- package/tools/audit/release-metadata.py +47 -1
- package/tools/audit/smoke_assertions.py +915 -13
- package/tools/migrations/refs-links-to-reference-pages.py +179 -0
|
@@ -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",
|
|
@@ -2667,6 +2805,321 @@ def assert_search_human_output(raw: str, *, context: str, cmd: list[str]) -> Non
|
|
|
2667
2805
|
)
|
|
2668
2806
|
|
|
2669
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
|
+
|
|
2670
3123
|
def assert_show_json_line(raw: str, *, context: str, cmd: list[str]) -> None:
|
|
2671
3124
|
assert_no_ansi(raw, cmd)
|
|
2672
3125
|
try:
|
|
@@ -4213,18 +4666,21 @@ def passing_main_help_output() -> str:
|
|
|
4213
4666
|
" design-ai help [command|--json]",
|
|
4214
4667
|
"",
|
|
4215
4668
|
" install Symlink design-ai into Claude Code (~/.claude)",
|
|
4216
|
-
" 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",
|
|
4217
4673
|
" show <file[:line]> [--lines N:M] [--context N] [--json] Print a corpus file or line range",
|
|
4218
4674
|
" route <brief|--from-file file|--stdin|--list|--eval-template|--eval> [--limit N]",
|
|
4219
4675
|
" 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]",
|
|
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]",
|
|
4221
4677
|
" 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]",
|
|
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]",
|
|
4223
4679
|
" Generate prompt plus bounded context and prompt-pack eval checkpoints",
|
|
4224
4680
|
" check <artifact.md|--stdin|--examples> [--route id|--all-routes] [--learn]",
|
|
4225
4681
|
" Check generated Markdown artifact quality; add --issues-only or --learn",
|
|
4226
4682
|
" 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]",
|
|
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]",
|
|
4228
4684
|
" Manage local learning preferences, usage reports, signal registry, agent backlog, skill proposals, and eval checkpoints for prompt personalization",
|
|
4229
4685
|
" workspace [--root path] [--learning-file path] [--learning-usage path] [--learning-eval path] [--strict] [--json]",
|
|
4230
4686
|
" Show read-only local dogfood readiness: git, repository, learning usage, eval checkpoints, and release scripts",
|
|
@@ -4326,6 +4782,7 @@ def passing_workspace_json() -> str:
|
|
|
4326
4782
|
"learningUsage": None,
|
|
4327
4783
|
"learningEval": None,
|
|
4328
4784
|
"learningRestoreBackups": None,
|
|
4785
|
+
"retrievalIndex": None,
|
|
4329
4786
|
"release": {
|
|
4330
4787
|
"packageName": "@design-ai/cli",
|
|
4331
4788
|
"version": EXPECTED_RELEASE_VERSION,
|
|
@@ -6409,6 +6866,44 @@ def assert_workspace_json(raw: str, *, context: str, cmd: list[str]) -> None:
|
|
|
6409
6866
|
if privacy.get("mutatesProfile") is not False:
|
|
6410
6867
|
raise SystemExit(f"workspace JSON after {context} learningRestoreBackups mutation privacy flag changed")
|
|
6411
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
|
+
|
|
6412
6907
|
release = assert_smoke_json_keys(
|
|
6413
6908
|
payload.get("release"),
|
|
6414
6909
|
EXPECTED_WORKSPACE_RELEASE_KEYS,
|
|
@@ -9149,6 +9644,364 @@ def run_self_test() -> None:
|
|
|
9149
9644
|
scope="smoke assertions",
|
|
9150
9645
|
)
|
|
9151
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
|
+
|
|
9152
10005
|
show_cmd = ["design-ai", "show", EXPECTED_CORPUS_SHOW_TARGET, "--context", "0", "--json"]
|
|
9153
10006
|
assert_show_json_line(passing_show_json(), context=context, cmd=show_cmd)
|
|
9154
10007
|
expect_self_test_failure(
|
|
@@ -11012,6 +11865,54 @@ def run_self_test() -> None:
|
|
|
11012
11865
|
expected="privacy flags changed",
|
|
11013
11866
|
scope="smoke assertions",
|
|
11014
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
|
+
)
|
|
11015
11916
|
assert_workspace_strict_failure_json(
|
|
11016
11917
|
passing_workspace_json(),
|
|
11017
11918
|
returncode=1,
|
|
@@ -11068,6 +11969,7 @@ def run_self_test() -> None:
|
|
|
11068
11969
|
"learningUsage": reordered_workspace_payload["learningUsage"],
|
|
11069
11970
|
"learningEval": reordered_workspace_payload["learningEval"],
|
|
11070
11971
|
"learningRestoreBackups": reordered_workspace_payload["learningRestoreBackups"],
|
|
11972
|
+
"retrievalIndex": reordered_workspace_payload["retrievalIndex"],
|
|
11071
11973
|
"release": reordered_workspace_payload["release"],
|
|
11072
11974
|
"nextActions": reordered_workspace_payload["nextActions"],
|
|
11073
11975
|
}
|