@productbrain/cli 0.1.0-beta.7 → 0.1.0-beta.70
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/README.md +98 -30
- package/dist/__tests__/adapters.test.d.ts +2 -0
- package/dist/__tests__/adapters.test.d.ts.map +1 -0
- package/dist/__tests__/adapters.test.js +417 -0
- package/dist/__tests__/adapters.test.js.map +1 -0
- package/dist/__tests__/audit.test.d.ts +2 -0
- package/dist/__tests__/audit.test.d.ts.map +1 -0
- package/dist/__tests__/audit.test.js +394 -0
- package/dist/__tests__/audit.test.js.map +1 -0
- package/dist/__tests__/batch-transformations.test.d.ts +2 -0
- package/dist/__tests__/batch-transformations.test.d.ts.map +1 -0
- package/dist/__tests__/batch-transformations.test.js +263 -0
- package/dist/__tests__/batch-transformations.test.js.map +1 -0
- package/dist/__tests__/capture.test.d.ts +2 -0
- package/dist/__tests__/capture.test.d.ts.map +1 -0
- package/dist/__tests__/capture.test.js +377 -0
- package/dist/__tests__/capture.test.js.map +1 -0
- package/dist/__tests__/config.test.d.ts +8 -0
- package/dist/__tests__/config.test.d.ts.map +1 -0
- package/dist/__tests__/config.test.js +166 -0
- package/dist/__tests__/config.test.js.map +1 -0
- package/dist/__tests__/constants.test.d.ts +2 -0
- package/dist/__tests__/constants.test.d.ts.map +1 -0
- package/dist/__tests__/constants.test.js +141 -0
- package/dist/__tests__/constants.test.js.map +1 -0
- package/dist/__tests__/constellation.test.d.ts +2 -0
- package/dist/__tests__/constellation.test.d.ts.map +1 -0
- package/dist/__tests__/constellation.test.js +254 -0
- package/dist/__tests__/constellation.test.js.map +1 -0
- package/dist/__tests__/context-strategy.test.d.ts +2 -0
- package/dist/__tests__/context-strategy.test.d.ts.map +1 -0
- package/dist/__tests__/context-strategy.test.js +79 -0
- package/dist/__tests__/context-strategy.test.js.map +1 -0
- package/dist/__tests__/errors.test.d.ts +2 -0
- package/dist/__tests__/errors.test.d.ts.map +1 -0
- package/dist/__tests__/errors.test.js +117 -0
- package/dist/__tests__/errors.test.js.map +1 -0
- package/dist/__tests__/experiment.test.d.ts +6 -0
- package/dist/__tests__/experiment.test.d.ts.map +1 -0
- package/dist/__tests__/experiment.test.js +69 -0
- package/dist/__tests__/experiment.test.js.map +1 -0
- package/dist/__tests__/fields.test.d.ts +2 -0
- package/dist/__tests__/fields.test.d.ts.map +1 -0
- package/dist/__tests__/fields.test.js +238 -0
- package/dist/__tests__/fields.test.js.map +1 -0
- package/dist/__tests__/glossary.test.d.ts +2 -0
- package/dist/__tests__/glossary.test.d.ts.map +1 -0
- package/dist/__tests__/glossary.test.js +32 -0
- package/dist/__tests__/glossary.test.js.map +1 -0
- package/dist/__tests__/handshake.test.d.ts +2 -0
- package/dist/__tests__/handshake.test.d.ts.map +1 -0
- package/dist/__tests__/handshake.test.js +196 -0
- package/dist/__tests__/handshake.test.js.map +1 -0
- package/dist/__tests__/ingest.test.js +98 -0
- package/dist/__tests__/ingest.test.js.map +1 -1
- package/dist/__tests__/init.test.d.ts +7 -0
- package/dist/__tests__/init.test.d.ts.map +1 -0
- package/dist/__tests__/init.test.js +146 -0
- package/dist/__tests__/init.test.js.map +1 -0
- package/dist/__tests__/login.test.d.ts +2 -0
- package/dist/__tests__/login.test.d.ts.map +1 -0
- package/dist/__tests__/login.test.js +167 -0
- package/dist/__tests__/login.test.js.map +1 -0
- package/dist/__tests__/onboarding-path-b.test.d.ts +2 -0
- package/dist/__tests__/onboarding-path-b.test.d.ts.map +1 -0
- package/dist/__tests__/onboarding-path-b.test.js +46 -0
- package/dist/__tests__/onboarding-path-b.test.js.map +1 -0
- package/dist/__tests__/onboarding.test.d.ts +6 -0
- package/dist/__tests__/onboarding.test.d.ts.map +1 -0
- package/dist/__tests__/onboarding.test.js +347 -0
- package/dist/__tests__/onboarding.test.js.map +1 -0
- package/dist/__tests__/orient.test.d.ts +2 -0
- package/dist/__tests__/orient.test.d.ts.map +1 -0
- package/dist/__tests__/orient.test.js +143 -0
- package/dist/__tests__/orient.test.js.map +1 -0
- package/dist/__tests__/profiles.test.d.ts +2 -0
- package/dist/__tests__/profiles.test.d.ts.map +1 -0
- package/dist/__tests__/profiles.test.js +168 -0
- package/dist/__tests__/profiles.test.js.map +1 -0
- package/dist/__tests__/promote.test.d.ts +2 -0
- package/dist/__tests__/promote.test.d.ts.map +1 -0
- package/dist/__tests__/promote.test.js +161 -0
- package/dist/__tests__/promote.test.js.map +1 -0
- package/dist/__tests__/prompts.test.d.ts +6 -0
- package/dist/__tests__/prompts.test.d.ts.map +1 -0
- package/dist/__tests__/prompts.test.js +146 -0
- package/dist/__tests__/prompts.test.js.map +1 -0
- package/dist/__tests__/proposals.test.d.ts +2 -0
- package/dist/__tests__/proposals.test.d.ts.map +1 -0
- package/dist/__tests__/proposals.test.js +167 -0
- package/dist/__tests__/proposals.test.js.map +1 -0
- package/dist/__tests__/relate.test.d.ts +2 -0
- package/dist/__tests__/relate.test.d.ts.map +1 -0
- package/dist/__tests__/relate.test.js +103 -0
- package/dist/__tests__/relate.test.js.map +1 -0
- package/dist/__tests__/repo-detect.test.d.ts +2 -0
- package/dist/__tests__/repo-detect.test.d.ts.map +1 -0
- package/dist/__tests__/repo-detect.test.js +215 -0
- package/dist/__tests__/repo-detect.test.js.map +1 -0
- package/dist/__tests__/runner.test.d.ts +2 -0
- package/dist/__tests__/runner.test.d.ts.map +1 -0
- package/dist/__tests__/runner.test.js +219 -0
- package/dist/__tests__/runner.test.js.map +1 -0
- package/dist/__tests__/session-touch.test.d.ts +2 -0
- package/dist/__tests__/session-touch.test.d.ts.map +1 -0
- package/dist/__tests__/session-touch.test.js +134 -0
- package/dist/__tests__/session-touch.test.js.map +1 -0
- package/dist/__tests__/session.test.d.ts +2 -0
- package/dist/__tests__/session.test.d.ts.map +1 -0
- package/dist/__tests__/session.test.js +46 -0
- package/dist/__tests__/session.test.js.map +1 -0
- package/dist/__tests__/setup.test.d.ts +2 -0
- package/dist/__tests__/setup.test.d.ts.map +1 -0
- package/dist/__tests__/setup.test.js +141 -0
- package/dist/__tests__/setup.test.js.map +1 -0
- package/dist/__tests__/spinner-labels.test.d.ts +2 -0
- package/dist/__tests__/spinner-labels.test.d.ts.map +1 -0
- package/dist/__tests__/spinner-labels.test.js +23 -0
- package/dist/__tests__/spinner-labels.test.js.map +1 -0
- package/dist/__tests__/strip.test.d.ts +2 -0
- package/dist/__tests__/strip.test.d.ts.map +1 -0
- package/dist/__tests__/strip.test.js +136 -0
- package/dist/__tests__/strip.test.js.map +1 -0
- package/dist/__tests__/surface-profiles.test.d.ts +2 -0
- package/dist/__tests__/surface-profiles.test.d.ts.map +1 -0
- package/dist/__tests__/surface-profiles.test.js +233 -0
- package/dist/__tests__/surface-profiles.test.js.map +1 -0
- package/dist/__tests__/update.test.d.ts +2 -0
- package/dist/__tests__/update.test.d.ts.map +1 -0
- package/dist/__tests__/update.test.js +228 -0
- package/dist/__tests__/update.test.js.map +1 -0
- package/dist/__tests__/workspace.test.d.ts +2 -0
- package/dist/__tests__/workspace.test.d.ts.map +1 -0
- package/dist/__tests__/workspace.test.js +308 -0
- package/dist/__tests__/workspace.test.js.map +1 -0
- package/dist/commands/accept.d.ts +18 -0
- package/dist/commands/accept.d.ts.map +1 -0
- package/dist/commands/accept.js +76 -0
- package/dist/commands/accept.js.map +1 -0
- package/dist/commands/admin/cockpit.d.ts +88 -0
- package/dist/commands/admin/cockpit.d.ts.map +1 -0
- package/dist/commands/admin/cockpit.js +409 -0
- package/dist/commands/admin/cockpit.js.map +1 -0
- package/dist/commands/admin/index.d.ts +21 -0
- package/dist/commands/admin/index.d.ts.map +1 -0
- package/dist/commands/admin/index.js +254 -0
- package/dist/commands/admin/index.js.map +1 -0
- package/dist/commands/admin/inspect.d.ts +21 -0
- package/dist/commands/admin/inspect.d.ts.map +1 -0
- package/dist/commands/admin/inspect.js +536 -0
- package/dist/commands/admin/inspect.js.map +1 -0
- package/dist/commands/admin/inspect.test.d.ts +7 -0
- package/dist/commands/admin/inspect.test.d.ts.map +1 -0
- package/dist/commands/admin/inspect.test.js +71 -0
- package/dist/commands/admin/inspect.test.js.map +1 -0
- package/dist/commands/admin/seed.d.ts +32 -0
- package/dist/commands/admin/seed.d.ts.map +1 -0
- package/dist/commands/admin/seed.js +527 -0
- package/dist/commands/admin/seed.js.map +1 -0
- package/dist/commands/admin/seed.test.d.ts +6 -0
- package/dist/commands/admin/seed.test.d.ts.map +1 -0
- package/dist/commands/admin/seed.test.js +65 -0
- package/dist/commands/admin/seed.test.js.map +1 -0
- package/dist/commands/audit.d.ts +25 -0
- package/dist/commands/audit.d.ts.map +1 -0
- package/dist/commands/audit.js +188 -0
- package/dist/commands/audit.js.map +1 -0
- package/dist/commands/brand-pack.d.ts +2 -0
- package/dist/commands/brand-pack.d.ts.map +1 -0
- package/dist/commands/brand-pack.js +25 -0
- package/dist/commands/brand-pack.js.map +1 -0
- package/dist/commands/brief.d.ts +28 -0
- package/dist/commands/brief.d.ts.map +1 -0
- package/dist/commands/brief.js +75 -0
- package/dist/commands/brief.js.map +1 -0
- package/dist/commands/capture.d.ts +30 -0
- package/dist/commands/capture.d.ts.map +1 -0
- package/dist/commands/capture.js +385 -0
- package/dist/commands/capture.js.map +1 -0
- package/dist/commands/chain-walk.d.ts +14 -0
- package/dist/commands/chain-walk.d.ts.map +1 -0
- package/dist/commands/chain-walk.js +38 -0
- package/dist/commands/chain-walk.js.map +1 -0
- package/dist/commands/changes.d.ts +11 -0
- package/dist/commands/changes.d.ts.map +1 -0
- package/dist/commands/changes.js +46 -0
- package/dist/commands/changes.js.map +1 -0
- package/dist/commands/codex-prep.d.ts +12 -0
- package/dist/commands/codex-prep.d.ts.map +1 -0
- package/dist/commands/codex-prep.js +122 -0
- package/dist/commands/codex-prep.js.map +1 -0
- package/dist/commands/collections.d.ts +22 -0
- package/dist/commands/collections.d.ts.map +1 -0
- package/dist/commands/collections.js +77 -0
- package/dist/commands/collections.js.map +1 -0
- package/dist/commands/connect-integration.test.d.ts +7 -0
- package/dist/commands/connect-integration.test.d.ts.map +1 -0
- package/dist/commands/connect-integration.test.js +200 -0
- package/dist/commands/connect-integration.test.js.map +1 -0
- package/dist/commands/connect.d.ts +21 -0
- package/dist/commands/connect.d.ts.map +1 -0
- package/dist/commands/connect.js +279 -0
- package/dist/commands/connect.js.map +1 -0
- package/dist/commands/connect.test.d.ts +6 -0
- package/dist/commands/connect.test.d.ts.map +1 -0
- package/dist/commands/connect.test.js +230 -0
- package/dist/commands/connect.test.js.map +1 -0
- package/dist/commands/constellation.d.ts +11 -0
- package/dist/commands/constellation.d.ts.map +1 -0
- package/dist/commands/constellation.js +33 -0
- package/dist/commands/constellation.js.map +1 -0
- package/dist/commands/context.d.ts +2 -1
- package/dist/commands/context.d.ts.map +1 -1
- package/dist/commands/context.js +20 -9
- package/dist/commands/context.js.map +1 -1
- package/dist/commands/cross-cut.d.ts +11 -0
- package/dist/commands/cross-cut.d.ts.map +1 -0
- package/dist/commands/cross-cut.js +23 -0
- package/dist/commands/cross-cut.js.map +1 -0
- package/dist/commands/doctor.d.ts +18 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +232 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/doctor.test.d.ts +8 -0
- package/dist/commands/doctor.test.d.ts.map +1 -0
- package/dist/commands/doctor.test.js +311 -0
- package/dist/commands/doctor.test.js.map +1 -0
- package/dist/commands/fields.d.ts +9 -0
- package/dist/commands/fields.d.ts.map +1 -0
- package/dist/commands/fields.js +30 -0
- package/dist/commands/fields.js.map +1 -0
- package/dist/commands/get.d.ts +8 -1
- package/dist/commands/get.d.ts.map +1 -1
- package/dist/commands/get.js +60 -7
- package/dist/commands/get.js.map +1 -1
- package/dist/commands/handshake.d.ts +28 -0
- package/dist/commands/handshake.d.ts.map +1 -0
- package/dist/commands/handshake.js +617 -0
- package/dist/commands/handshake.js.map +1 -0
- package/dist/commands/ingest.d.ts +8 -2
- package/dist/commands/ingest.d.ts.map +1 -1
- package/dist/commands/ingest.js +148 -25
- package/dist/commands/ingest.js.map +1 -1
- package/dist/commands/init.d.ts +14 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +117 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/login.d.ts +4 -0
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +101 -38
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/orient.d.ts +107 -1
- package/dist/commands/orient.d.ts.map +1 -1
- package/dist/commands/orient.js +24 -8
- package/dist/commands/orient.js.map +1 -1
- package/dist/commands/profile.d.ts +24 -0
- package/dist/commands/profile.d.ts.map +1 -0
- package/dist/commands/profile.js +82 -0
- package/dist/commands/profile.js.map +1 -0
- package/dist/commands/promote.d.ts +12 -0
- package/dist/commands/promote.d.ts.map +1 -0
- package/dist/commands/promote.js +90 -0
- package/dist/commands/promote.js.map +1 -0
- package/dist/commands/proposals.d.ts +9 -0
- package/dist/commands/proposals.d.ts.map +1 -0
- package/dist/commands/proposals.js +24 -0
- package/dist/commands/proposals.js.map +1 -0
- package/dist/commands/reject.d.ts +14 -0
- package/dist/commands/reject.d.ts.map +1 -0
- package/dist/commands/reject.js +43 -0
- package/dist/commands/reject.js.map +1 -0
- package/dist/commands/relate.d.ts +16 -0
- package/dist/commands/relate.d.ts.map +1 -0
- package/dist/commands/relate.js +98 -0
- package/dist/commands/relate.js.map +1 -0
- package/dist/commands/search.d.ts +1 -0
- package/dist/commands/search.d.ts.map +1 -1
- package/dist/commands/search.js +5 -3
- package/dist/commands/search.js.map +1 -1
- package/dist/commands/session.d.ts +20 -0
- package/dist/commands/session.d.ts.map +1 -0
- package/dist/commands/session.js +148 -0
- package/dist/commands/session.js.map +1 -0
- package/dist/commands/setup.d.ts +15 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +168 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/update.d.ts +17 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/update.js +178 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/commands/verify.d.ts +13 -0
- package/dist/commands/verify.d.ts.map +1 -0
- package/dist/commands/verify.js +49 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/commands/workspace.d.ts +41 -0
- package/dist/commands/workspace.d.ts.map +1 -0
- package/dist/commands/workspace.js +239 -0
- package/dist/commands/workspace.js.map +1 -0
- package/dist/formatters/audit.d.ts +46 -0
- package/dist/formatters/audit.d.ts.map +1 -0
- package/dist/formatters/audit.js +81 -0
- package/dist/formatters/audit.js.map +1 -0
- package/dist/formatters/brief.d.ts +112 -0
- package/dist/formatters/brief.d.ts.map +1 -0
- package/dist/formatters/brief.js +179 -0
- package/dist/formatters/brief.js.map +1 -0
- package/dist/formatters/capture.d.ts +48 -0
- package/dist/formatters/capture.d.ts.map +1 -0
- package/dist/formatters/capture.js +77 -0
- package/dist/formatters/capture.js.map +1 -0
- package/dist/formatters/chain-walk.d.ts +33 -0
- package/dist/formatters/chain-walk.d.ts.map +1 -0
- package/dist/formatters/chain-walk.js +54 -0
- package/dist/formatters/chain-walk.js.map +1 -0
- package/dist/formatters/changes.d.ts +25 -0
- package/dist/formatters/changes.d.ts.map +1 -0
- package/dist/formatters/changes.js +60 -0
- package/dist/formatters/changes.js.map +1 -0
- package/dist/formatters/collections.d.ts +40 -0
- package/dist/formatters/collections.d.ts.map +1 -0
- package/dist/formatters/collections.js +93 -0
- package/dist/formatters/collections.js.map +1 -0
- package/dist/formatters/constellation.d.ts +34 -0
- package/dist/formatters/constellation.d.ts.map +1 -0
- package/dist/formatters/constellation.js +38 -0
- package/dist/formatters/constellation.js.map +1 -0
- package/dist/formatters/cross-cut.d.ts +21 -0
- package/dist/formatters/cross-cut.d.ts.map +1 -0
- package/dist/formatters/cross-cut.js +32 -0
- package/dist/formatters/cross-cut.js.map +1 -0
- package/dist/formatters/entry.d.ts +11 -4
- package/dist/formatters/entry.d.ts.map +1 -1
- package/dist/formatters/entry.js +24 -8
- package/dist/formatters/entry.js.map +1 -1
- package/dist/formatters/fields.d.ts +32 -0
- package/dist/formatters/fields.d.ts.map +1 -0
- package/dist/formatters/fields.js +49 -0
- package/dist/formatters/fields.js.map +1 -0
- package/dist/formatters/handshake.d.ts +24 -0
- package/dist/formatters/handshake.d.ts.map +1 -0
- package/dist/formatters/handshake.js +69 -0
- package/dist/formatters/handshake.js.map +1 -0
- package/dist/formatters/orient.d.ts +104 -1
- package/dist/formatters/orient.d.ts.map +1 -1
- package/dist/formatters/orient.js +140 -17
- package/dist/formatters/orient.js.map +1 -1
- package/dist/formatters/promote.d.ts +30 -0
- package/dist/formatters/promote.d.ts.map +1 -0
- package/dist/formatters/promote.js +39 -0
- package/dist/formatters/promote.js.map +1 -0
- package/dist/formatters/proposals.d.ts +45 -0
- package/dist/formatters/proposals.d.ts.map +1 -0
- package/dist/formatters/proposals.js +62 -0
- package/dist/formatters/proposals.js.map +1 -0
- package/dist/formatters/relate.d.ts +14 -0
- package/dist/formatters/relate.d.ts.map +1 -0
- package/dist/formatters/relate.js +16 -0
- package/dist/formatters/relate.js.map +1 -0
- package/dist/formatters/search.d.ts +0 -4
- package/dist/formatters/search.d.ts.map +1 -1
- package/dist/formatters/search.js +4 -1
- package/dist/formatters/search.js.map +1 -1
- package/dist/formatters/session.d.ts +11 -0
- package/dist/formatters/session.d.ts.map +1 -0
- package/dist/formatters/session.js +53 -0
- package/dist/formatters/session.js.map +1 -0
- package/dist/formatters/update.d.ts +17 -0
- package/dist/formatters/update.d.ts.map +1 -0
- package/dist/formatters/update.js +45 -0
- package/dist/formatters/update.js.map +1 -0
- package/dist/formatters/verify.d.ts +11 -0
- package/dist/formatters/verify.d.ts.map +1 -0
- package/dist/formatters/verify.js +11 -0
- package/dist/formatters/verify.js.map +1 -0
- package/dist/generators/__tests__/surface-profiles.test.d.ts +2 -0
- package/dist/generators/__tests__/surface-profiles.test.d.ts.map +1 -0
- package/dist/generators/__tests__/surface-profiles.test.js +89 -0
- package/dist/generators/__tests__/surface-profiles.test.js.map +1 -0
- package/dist/generators/adapters.d.ts +44 -0
- package/dist/generators/adapters.d.ts.map +1 -0
- package/dist/generators/adapters.js +290 -0
- package/dist/generators/adapters.js.map +1 -0
- package/dist/generators/adapters.test.d.ts +2 -0
- package/dist/generators/adapters.test.d.ts.map +1 -0
- package/dist/generators/adapters.test.js +27 -0
- package/dist/generators/adapters.test.js.map +1 -0
- package/dist/generators/archetypes.d.ts +52 -0
- package/dist/generators/archetypes.d.ts.map +1 -0
- package/dist/generators/archetypes.js +153 -0
- package/dist/generators/archetypes.js.map +1 -0
- package/dist/generators/archetypes.test.d.ts +2 -0
- package/dist/generators/archetypes.test.d.ts.map +1 -0
- package/dist/generators/archetypes.test.js +237 -0
- package/dist/generators/archetypes.test.js.map +1 -0
- package/dist/generators/briefing-md.d.ts +8 -0
- package/dist/generators/briefing-md.d.ts.map +1 -0
- package/dist/generators/briefing-md.js +51 -0
- package/dist/generators/briefing-md.js.map +1 -0
- package/dist/generators/chain-classifier.d.ts +49 -0
- package/dist/generators/chain-classifier.d.ts.map +1 -0
- package/dist/generators/chain-classifier.js +180 -0
- package/dist/generators/chain-classifier.js.map +1 -0
- package/dist/generators/chain-classifier.test.d.ts +2 -0
- package/dist/generators/chain-classifier.test.d.ts.map +1 -0
- package/dist/generators/chain-classifier.test.js +257 -0
- package/dist/generators/chain-classifier.test.js.map +1 -0
- package/dist/generators/chain-rules.d.ts +42 -0
- package/dist/generators/chain-rules.d.ts.map +1 -0
- package/dist/generators/chain-rules.js +144 -0
- package/dist/generators/chain-rules.js.map +1 -0
- package/dist/generators/chain-rules.test.d.ts +2 -0
- package/dist/generators/chain-rules.test.d.ts.map +1 -0
- package/dist/generators/chain-rules.test.js +179 -0
- package/dist/generators/chain-rules.test.js.map +1 -0
- package/dist/generators/context-md.d.ts +8 -0
- package/dist/generators/context-md.d.ts.map +1 -0
- package/dist/generators/context-md.js +134 -0
- package/dist/generators/context-md.js.map +1 -0
- package/dist/generators/handshake-diff.d.ts +67 -0
- package/dist/generators/handshake-diff.d.ts.map +1 -0
- package/dist/generators/handshake-diff.js +183 -0
- package/dist/generators/handshake-diff.js.map +1 -0
- package/dist/generators/handshake-diff.test.d.ts +2 -0
- package/dist/generators/handshake-diff.test.d.ts.map +1 -0
- package/dist/generators/handshake-diff.test.js +264 -0
- package/dist/generators/handshake-diff.test.js.map +1 -0
- package/dist/generators/portable-knowledge.d.ts +143 -0
- package/dist/generators/portable-knowledge.d.ts.map +1 -0
- package/dist/generators/portable-knowledge.js +504 -0
- package/dist/generators/portable-knowledge.js.map +1 -0
- package/dist/generators/portable-knowledge.test.d.ts +2 -0
- package/dist/generators/portable-knowledge.test.d.ts.map +1 -0
- package/dist/generators/portable-knowledge.test.js +927 -0
- package/dist/generators/portable-knowledge.test.js.map +1 -0
- package/dist/generators/surface-profiles.d.ts +49 -0
- package/dist/generators/surface-profiles.d.ts.map +1 -0
- package/dist/generators/surface-profiles.js +98 -0
- package/dist/generators/surface-profiles.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +642 -37
- package/dist/index.js.map +1 -1
- package/dist/lib/activation.d.ts +28 -0
- package/dist/lib/activation.d.ts.map +1 -0
- package/dist/lib/activation.js +57 -0
- package/dist/lib/activation.js.map +1 -0
- package/dist/lib/activation.test.d.ts +6 -0
- package/dist/lib/activation.test.d.ts.map +1 -0
- package/dist/lib/activation.test.js +121 -0
- package/dist/lib/activation.test.js.map +1 -0
- package/dist/lib/client.d.ts +61 -0
- package/dist/lib/client.d.ts.map +1 -1
- package/dist/lib/client.js +258 -12
- package/dist/lib/client.js.map +1 -1
- package/dist/lib/config.d.ts +84 -4
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +322 -42
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/constants.d.ts +42 -0
- package/dist/lib/constants.d.ts.map +1 -0
- package/dist/lib/constants.js +76 -0
- package/dist/lib/constants.js.map +1 -0
- package/dist/lib/conversation-engine.d.ts +45 -0
- package/dist/lib/conversation-engine.d.ts.map +1 -0
- package/dist/lib/conversation-engine.js +112 -0
- package/dist/lib/conversation-engine.js.map +1 -0
- package/dist/lib/conversation-phases.d.ts +59 -0
- package/dist/lib/conversation-phases.d.ts.map +1 -0
- package/dist/lib/conversation-phases.js +11 -0
- package/dist/lib/conversation-phases.js.map +1 -0
- package/dist/lib/conversation-signals.d.ts +30 -0
- package/dist/lib/conversation-signals.d.ts.map +1 -0
- package/dist/lib/conversation-signals.js +64 -0
- package/dist/lib/conversation-signals.js.map +1 -0
- package/dist/lib/deployment.d.ts +23 -0
- package/dist/lib/deployment.d.ts.map +1 -0
- package/dist/lib/deployment.js +78 -0
- package/dist/lib/deployment.js.map +1 -0
- package/dist/lib/deployment.test.d.ts +5 -0
- package/dist/lib/deployment.test.d.ts.map +1 -0
- package/dist/lib/deployment.test.js +54 -0
- package/dist/lib/deployment.test.js.map +1 -0
- package/dist/lib/errors.d.ts +58 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +67 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/experiment.d.ts +18 -0
- package/dist/lib/experiment.d.ts.map +1 -0
- package/dist/lib/experiment.js +28 -0
- package/dist/lib/experiment.js.map +1 -0
- package/dist/lib/format.d.ts +10 -0
- package/dist/lib/format.d.ts.map +1 -0
- package/dist/lib/format.js +27 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/glossary.d.ts +19 -0
- package/dist/lib/glossary.d.ts.map +1 -0
- package/dist/lib/glossary.js +53 -0
- package/dist/lib/glossary.js.map +1 -0
- package/dist/lib/onboarding-path-b.d.ts +10 -0
- package/dist/lib/onboarding-path-b.d.ts.map +1 -0
- package/dist/lib/onboarding-path-b.js +214 -0
- package/dist/lib/onboarding-path-b.js.map +1 -0
- package/dist/lib/onboarding-phases.d.ts +9 -0
- package/dist/lib/onboarding-phases.d.ts.map +1 -0
- package/dist/lib/onboarding-phases.js +120 -0
- package/dist/lib/onboarding-phases.js.map +1 -0
- package/dist/lib/onboarding-shared.d.ts +81 -0
- package/dist/lib/onboarding-shared.d.ts.map +1 -0
- package/dist/lib/onboarding-shared.js +190 -0
- package/dist/lib/onboarding-shared.js.map +1 -0
- package/dist/lib/onboarding-topics.d.ts +27 -0
- package/dist/lib/onboarding-topics.d.ts.map +1 -0
- package/dist/lib/onboarding-topics.js +57 -0
- package/dist/lib/onboarding-topics.js.map +1 -0
- package/dist/lib/onboarding.d.ts +17 -0
- package/dist/lib/onboarding.d.ts.map +1 -0
- package/dist/lib/onboarding.js +350 -0
- package/dist/lib/onboarding.js.map +1 -0
- package/dist/lib/profiles.d.ts +39 -0
- package/dist/lib/profiles.d.ts.map +1 -0
- package/dist/lib/profiles.js +185 -0
- package/dist/lib/profiles.js.map +1 -0
- package/dist/lib/prompts.d.ts +65 -0
- package/dist/lib/prompts.d.ts.map +1 -0
- package/dist/lib/prompts.js +132 -0
- package/dist/lib/prompts.js.map +1 -0
- package/dist/lib/repo-detect.d.ts +33 -0
- package/dist/lib/repo-detect.d.ts.map +1 -0
- package/dist/lib/repo-detect.js +83 -0
- package/dist/lib/repo-detect.js.map +1 -0
- package/dist/lib/runner.d.ts +33 -0
- package/dist/lib/runner.d.ts.map +1 -0
- package/dist/lib/runner.js +79 -0
- package/dist/lib/runner.js.map +1 -0
- package/dist/lib/session.d.ts +17 -0
- package/dist/lib/session.d.ts.map +1 -0
- package/dist/lib/session.js +43 -0
- package/dist/lib/session.js.map +1 -0
- package/dist/lib/spinner.d.ts +27 -0
- package/dist/lib/spinner.d.ts.map +1 -0
- package/dist/lib/spinner.js +76 -0
- package/dist/lib/spinner.js.map +1 -0
- package/dist/lib/spinner.test.d.ts +2 -0
- package/dist/lib/spinner.test.d.ts.map +1 -0
- package/dist/lib/spinner.test.js +39 -0
- package/dist/lib/spinner.test.js.map +1 -0
- package/dist/lib/strip.d.ts +12 -0
- package/dist/lib/strip.d.ts.map +1 -0
- package/dist/lib/strip.js +41 -0
- package/dist/lib/strip.js.map +1 -0
- package/dist/lib/style.d.ts +94 -0
- package/dist/lib/style.d.ts.map +1 -0
- package/dist/lib/style.js +167 -0
- package/dist/lib/style.js.map +1 -0
- package/dist/lib/style.test.d.ts +7 -0
- package/dist/lib/style.test.d.ts.map +1 -0
- package/dist/lib/style.test.js +263 -0
- package/dist/lib/style.test.js.map +1 -0
- package/dist/lib/telemetry.d.ts +15 -0
- package/dist/lib/telemetry.d.ts.map +1 -0
- package/dist/lib/telemetry.js +47 -0
- package/dist/lib/telemetry.js.map +1 -0
- package/dist/lib/tokenConstants.d.ts +17 -0
- package/dist/lib/tokenConstants.d.ts.map +1 -0
- package/dist/lib/tokenConstants.js +17 -0
- package/dist/lib/tokenConstants.js.map +1 -0
- package/dist/lib/wizard-surfaces.d.ts +47 -0
- package/dist/lib/wizard-surfaces.d.ts.map +1 -0
- package/dist/lib/wizard-surfaces.js +176 -0
- package/dist/lib/wizard-surfaces.js.map +1 -0
- package/dist/lib/wizard-surfaces.test.d.ts +2 -0
- package/dist/lib/wizard-surfaces.test.d.ts.map +1 -0
- package/dist/lib/wizard-surfaces.test.js +127 -0
- package/dist/lib/wizard-surfaces.test.js.map +1 -0
- package/dist/lib/wizard-trust.d.ts +31 -0
- package/dist/lib/wizard-trust.d.ts.map +1 -0
- package/dist/lib/wizard-trust.js +66 -0
- package/dist/lib/wizard-trust.js.map +1 -0
- package/dist/lib/wizard-trust.test.d.ts +2 -0
- package/dist/lib/wizard-trust.test.d.ts.map +1 -0
- package/dist/lib/wizard-trust.test.js +32 -0
- package/dist/lib/wizard-trust.test.js.map +1 -0
- package/dist/lib/workspace-probe.d.ts +16 -0
- package/dist/lib/workspace-probe.d.ts.map +1 -0
- package/dist/lib/workspace-probe.js +33 -0
- package/dist/lib/workspace-probe.js.map +1 -0
- package/package.json +13 -4
- package/templates/archetypes/boundary.md +23 -0
- package/templates/archetypes/constraint.md +23 -0
- package/templates/archetypes/convention.md +23 -0
- package/templates/archetypes/policy.md +23 -0
- package/templates/archetypes/quality-gate.md +23 -0
- package/templates/archetypes/workflow.md +23 -0
- package/templates/general/code-integrity.md +11 -0
- package/templates/general/getting-started.md +12 -0
- package/templates/node-ts/code-integrity.md +13 -0
- package/templates/node-ts/testing.md +12 -0
- package/templates/python/code-integrity.md +13 -0
- package/templates/python/testing.md +12 -0
|
@@ -0,0 +1,927 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* portable-knowledge — unit tests.
|
|
3
|
+
* BET-169: transport-aware skill dispatch, target filtering, and transport section stripping.
|
|
4
|
+
*/
|
|
5
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
// vi.mock calls are hoisted — use vi.hoisted() for constants referenced inside factories.
|
|
8
|
+
const { vfs } = vi.hoisted(() => ({
|
|
9
|
+
vfs: {},
|
|
10
|
+
}));
|
|
11
|
+
vi.mock('fs', () => ({
|
|
12
|
+
mkdirSync: vi.fn(),
|
|
13
|
+
writeFileSync: vi.fn((path, content) => {
|
|
14
|
+
vfs[path] = content;
|
|
15
|
+
}),
|
|
16
|
+
existsSync: vi.fn((path) => {
|
|
17
|
+
// Check if the path itself or any child key exists (for directory checks)
|
|
18
|
+
if (path in vfs)
|
|
19
|
+
return true;
|
|
20
|
+
// For directory checks: return true if any key starts with path + '/'
|
|
21
|
+
return Object.keys(vfs).some((k) => k.startsWith(path + '/'));
|
|
22
|
+
}),
|
|
23
|
+
readFileSync: vi.fn((path, _enc) => {
|
|
24
|
+
if (path in vfs)
|
|
25
|
+
return vfs[path];
|
|
26
|
+
throw Object.assign(new Error(`ENOENT: no such file '${path}'`), { code: 'ENOENT' });
|
|
27
|
+
}),
|
|
28
|
+
readdirSync: vi.fn((dir) => {
|
|
29
|
+
const prefix = dir.endsWith('/') ? dir : dir + '/';
|
|
30
|
+
const files = new Set();
|
|
31
|
+
for (const key of Object.keys(vfs)) {
|
|
32
|
+
if (key.startsWith(prefix)) {
|
|
33
|
+
const rest = key.slice(prefix.length);
|
|
34
|
+
const parts = rest.split('/');
|
|
35
|
+
if (parts.length === 1)
|
|
36
|
+
files.add(parts[0]);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return [...files];
|
|
40
|
+
}),
|
|
41
|
+
}));
|
|
42
|
+
import { readCanonicalSkills, readCanonicalRules, shouldEmitToTarget, stripTransportSections, filterByLevel, evaluateConditions, generateCursorSkill, generateCursorRule, generateClaudeRule, generateCodexSkill, generateCodexSkillIndex, generateClaudeSkillRouter, } from './portable-knowledge.js';
|
|
43
|
+
const PB_DIR = '/tmp/pb-test/.productbrain';
|
|
44
|
+
describe('readCanonicalSkills', () => {
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
Object.keys(vfs).forEach((k) => delete vfs[k]);
|
|
47
|
+
});
|
|
48
|
+
it('parses targets from frontmatter', () => {
|
|
49
|
+
vfs[join(PB_DIR, 'skills', 'test-skill.md')] = `---
|
|
50
|
+
name: test-skill
|
|
51
|
+
description: A test skill
|
|
52
|
+
triggers:
|
|
53
|
+
- test
|
|
54
|
+
targets:
|
|
55
|
+
- claude
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
# Test Skill Body
|
|
59
|
+
`;
|
|
60
|
+
const skills = readCanonicalSkills(PB_DIR);
|
|
61
|
+
expect(skills).toHaveLength(1);
|
|
62
|
+
expect(skills[0].targets).toEqual(['claude']);
|
|
63
|
+
});
|
|
64
|
+
it('returns undefined targets when not specified in frontmatter', () => {
|
|
65
|
+
vfs[join(PB_DIR, 'skills', 'universal-skill.md')] = `---
|
|
66
|
+
name: universal-skill
|
|
67
|
+
description: A universal skill
|
|
68
|
+
triggers:
|
|
69
|
+
- universal
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
# Universal Skill Body
|
|
73
|
+
`;
|
|
74
|
+
const skills = readCanonicalSkills(PB_DIR);
|
|
75
|
+
expect(skills).toHaveLength(1);
|
|
76
|
+
expect(skills[0].targets).toBeUndefined();
|
|
77
|
+
});
|
|
78
|
+
it('parses multiple targets', () => {
|
|
79
|
+
vfs[join(PB_DIR, 'skills', 'multi-target.md')] = `---
|
|
80
|
+
name: multi-target
|
|
81
|
+
description: Multi-target skill
|
|
82
|
+
triggers:
|
|
83
|
+
- multi
|
|
84
|
+
targets:
|
|
85
|
+
- claude
|
|
86
|
+
- cursor
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
# Multi-Target Body
|
|
90
|
+
`;
|
|
91
|
+
const skills = readCanonicalSkills(PB_DIR);
|
|
92
|
+
expect(skills).toHaveLength(1);
|
|
93
|
+
expect(skills[0].targets).toEqual(['claude', 'cursor']);
|
|
94
|
+
});
|
|
95
|
+
it('parses level from frontmatter', () => {
|
|
96
|
+
vfs[join(PB_DIR, 'skills', 'leveled-skill.md')] = `---
|
|
97
|
+
name: leveled-skill
|
|
98
|
+
description: A leveled skill
|
|
99
|
+
level: core
|
|
100
|
+
triggers:
|
|
101
|
+
- leveled
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
# Leveled Skill Body
|
|
105
|
+
`;
|
|
106
|
+
const skills = readCanonicalSkills(PB_DIR);
|
|
107
|
+
expect(skills).toHaveLength(1);
|
|
108
|
+
expect(skills[0].level).toBe('core');
|
|
109
|
+
});
|
|
110
|
+
it('returns undefined level when not specified in frontmatter', () => {
|
|
111
|
+
vfs[join(PB_DIR, 'skills', 'no-level-skill.md')] = `---
|
|
112
|
+
name: no-level-skill
|
|
113
|
+
description: A skill without level
|
|
114
|
+
triggers:
|
|
115
|
+
- nolevel
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
# No Level Skill Body
|
|
119
|
+
`;
|
|
120
|
+
const skills = readCanonicalSkills(PB_DIR);
|
|
121
|
+
expect(skills).toHaveLength(1);
|
|
122
|
+
expect(skills[0].level).toBeUndefined();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
describe('shouldEmitToTarget', () => {
|
|
126
|
+
it('returns true when targets is undefined (emit to all)', () => {
|
|
127
|
+
const skill = {
|
|
128
|
+
name: 'test',
|
|
129
|
+
description: '',
|
|
130
|
+
triggers: [],
|
|
131
|
+
body: '',
|
|
132
|
+
sourcePath: '',
|
|
133
|
+
};
|
|
134
|
+
expect(shouldEmitToTarget(skill, 'claude')).toBe(true);
|
|
135
|
+
expect(shouldEmitToTarget(skill, 'cursor')).toBe(true);
|
|
136
|
+
expect(shouldEmitToTarget(skill, 'copilot')).toBe(true);
|
|
137
|
+
expect(shouldEmitToTarget(skill, 'codex')).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
it('returns true when target is in the list', () => {
|
|
140
|
+
const skill = {
|
|
141
|
+
name: 'test',
|
|
142
|
+
description: '',
|
|
143
|
+
triggers: [],
|
|
144
|
+
body: '',
|
|
145
|
+
sourcePath: '',
|
|
146
|
+
targets: ['claude'],
|
|
147
|
+
};
|
|
148
|
+
expect(shouldEmitToTarget(skill, 'claude')).toBe(true);
|
|
149
|
+
});
|
|
150
|
+
it('returns false when target is not in the list', () => {
|
|
151
|
+
const skill = {
|
|
152
|
+
name: 'test',
|
|
153
|
+
description: '',
|
|
154
|
+
triggers: [],
|
|
155
|
+
body: '',
|
|
156
|
+
sourcePath: '',
|
|
157
|
+
targets: ['claude'],
|
|
158
|
+
};
|
|
159
|
+
expect(shouldEmitToTarget(skill, 'cursor')).toBe(false);
|
|
160
|
+
expect(shouldEmitToTarget(skill, 'copilot')).toBe(false);
|
|
161
|
+
});
|
|
162
|
+
it('works for CanonicalRule as well', () => {
|
|
163
|
+
const rule = {
|
|
164
|
+
name: 'test-rule',
|
|
165
|
+
description: '',
|
|
166
|
+
autoApply: true,
|
|
167
|
+
body: '',
|
|
168
|
+
sourcePath: '',
|
|
169
|
+
targets: ['cursor'],
|
|
170
|
+
};
|
|
171
|
+
expect(shouldEmitToTarget(rule, 'cursor')).toBe(true);
|
|
172
|
+
expect(shouldEmitToTarget(rule, 'claude')).toBe(false);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
describe('stripTransportSections', () => {
|
|
176
|
+
it('keeps content for the matching target and removes delimiters', () => {
|
|
177
|
+
const body = `Universal content above.
|
|
178
|
+
|
|
179
|
+
<!-- transport:claude -->
|
|
180
|
+
Claude-specific instructions here.
|
|
181
|
+
<!-- /transport -->
|
|
182
|
+
|
|
183
|
+
Universal content below.`;
|
|
184
|
+
const result = stripTransportSections(body, 'claude');
|
|
185
|
+
expect(result).toContain('Claude-specific instructions here.');
|
|
186
|
+
expect(result).toContain('Universal content above.');
|
|
187
|
+
expect(result).toContain('Universal content below.');
|
|
188
|
+
expect(result).not.toContain('<!-- transport:claude -->');
|
|
189
|
+
expect(result).not.toContain('<!-- /transport -->');
|
|
190
|
+
});
|
|
191
|
+
it('removes blocks for other targets entirely', () => {
|
|
192
|
+
const body = `Universal content.
|
|
193
|
+
|
|
194
|
+
<!-- transport:cursor -->
|
|
195
|
+
Cursor-only instructions.
|
|
196
|
+
<!-- /transport -->
|
|
197
|
+
|
|
198
|
+
More universal.`;
|
|
199
|
+
const result = stripTransportSections(body, 'claude');
|
|
200
|
+
expect(result).toContain('Universal content.');
|
|
201
|
+
expect(result).toContain('More universal.');
|
|
202
|
+
expect(result).not.toContain('Cursor-only instructions.');
|
|
203
|
+
expect(result).not.toContain('<!-- transport:cursor -->');
|
|
204
|
+
});
|
|
205
|
+
it('handles multiple transport blocks for different targets', () => {
|
|
206
|
+
const body = `# Heading
|
|
207
|
+
|
|
208
|
+
<!-- transport:claude -->
|
|
209
|
+
Claude dispatch: use Agent tool.
|
|
210
|
+
<!-- /transport -->
|
|
211
|
+
|
|
212
|
+
<!-- transport:cursor -->
|
|
213
|
+
Cursor dispatch: use fresh conversation.
|
|
214
|
+
<!-- /transport -->
|
|
215
|
+
|
|
216
|
+
## Footer`;
|
|
217
|
+
const claudeResult = stripTransportSections(body, 'claude');
|
|
218
|
+
expect(claudeResult).toContain('Claude dispatch: use Agent tool.');
|
|
219
|
+
expect(claudeResult).not.toContain('Cursor dispatch: use fresh conversation.');
|
|
220
|
+
expect(claudeResult).toContain('# Heading');
|
|
221
|
+
expect(claudeResult).toContain('## Footer');
|
|
222
|
+
const cursorResult = stripTransportSections(body, 'cursor');
|
|
223
|
+
expect(cursorResult).toContain('Cursor dispatch: use fresh conversation.');
|
|
224
|
+
expect(cursorResult).not.toContain('Claude dispatch: use Agent tool.');
|
|
225
|
+
expect(cursorResult).toContain('# Heading');
|
|
226
|
+
expect(cursorResult).toContain('## Footer');
|
|
227
|
+
});
|
|
228
|
+
it('keeps universal content untouched when no transport blocks exist', () => {
|
|
229
|
+
const body = `# Just a normal skill
|
|
230
|
+
|
|
231
|
+
No transport sections here.
|
|
232
|
+
|
|
233
|
+
## Section 2
|
|
234
|
+
|
|
235
|
+
More content.`;
|
|
236
|
+
const result = stripTransportSections(body, 'claude');
|
|
237
|
+
expect(result).toBe(body);
|
|
238
|
+
});
|
|
239
|
+
it('handles markdown and code blocks inside transport sections', () => {
|
|
240
|
+
const body = `Universal.
|
|
241
|
+
|
|
242
|
+
<!-- transport:claude -->
|
|
243
|
+
**Bold text** and \`inline code\`.
|
|
244
|
+
|
|
245
|
+
\`\`\`bash
|
|
246
|
+
git diff main..HEAD
|
|
247
|
+
\`\`\`
|
|
248
|
+
|
|
249
|
+
- List item 1
|
|
250
|
+
- List item 2
|
|
251
|
+
<!-- /transport -->
|
|
252
|
+
|
|
253
|
+
End.`;
|
|
254
|
+
const result = stripTransportSections(body, 'claude');
|
|
255
|
+
expect(result).toContain('**Bold text** and `inline code`.');
|
|
256
|
+
expect(result).toContain('git diff main..HEAD');
|
|
257
|
+
expect(result).toContain('- List item 1');
|
|
258
|
+
expect(result).toContain('End.');
|
|
259
|
+
});
|
|
260
|
+
it('handles transport sections with no trailing newline after closing tag', () => {
|
|
261
|
+
const body = `Before.
|
|
262
|
+
<!-- transport:claude -->
|
|
263
|
+
Content.
|
|
264
|
+
<!-- /transport -->After.`;
|
|
265
|
+
const result = stripTransportSections(body, 'claude');
|
|
266
|
+
expect(result).toContain('Content.');
|
|
267
|
+
expect(result).toContain('After.');
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
describe('generateCursorSkill (transport stripping)', () => {
|
|
271
|
+
it('strips non-cursor transport sections from generated output', () => {
|
|
272
|
+
const skill = {
|
|
273
|
+
name: 'test-skill',
|
|
274
|
+
description: 'Test skill',
|
|
275
|
+
triggers: ['test'],
|
|
276
|
+
body: `# Heading
|
|
277
|
+
|
|
278
|
+
<!-- transport:claude -->
|
|
279
|
+
Claude-only content.
|
|
280
|
+
<!-- /transport -->
|
|
281
|
+
|
|
282
|
+
<!-- transport:cursor -->
|
|
283
|
+
Cursor-only content.
|
|
284
|
+
<!-- /transport -->
|
|
285
|
+
|
|
286
|
+
Universal content.`,
|
|
287
|
+
sourcePath: '/tmp/.productbrain/skills/test-skill.md',
|
|
288
|
+
};
|
|
289
|
+
const output = generateCursorSkill(skill);
|
|
290
|
+
expect(output).toContain('Cursor-only content.');
|
|
291
|
+
expect(output).not.toContain('Claude-only content.');
|
|
292
|
+
expect(output).toContain('Universal content.');
|
|
293
|
+
expect(output).not.toContain('<!-- transport:');
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
describe('generateCodexSkill / generateCodexSkillIndex', () => {
|
|
297
|
+
it('strips non-codex transport sections from generated Codex skill output', () => {
|
|
298
|
+
const skill = {
|
|
299
|
+
name: 'test-skill',
|
|
300
|
+
description: 'Test skill',
|
|
301
|
+
triggers: ['test'],
|
|
302
|
+
body: `# Heading
|
|
303
|
+
|
|
304
|
+
<!-- transport:claude -->
|
|
305
|
+
Claude-only content.
|
|
306
|
+
<!-- /transport -->
|
|
307
|
+
|
|
308
|
+
<!-- transport:codex -->
|
|
309
|
+
Codex-only content.
|
|
310
|
+
<!-- /transport -->
|
|
311
|
+
|
|
312
|
+
Universal content.`,
|
|
313
|
+
sourcePath: '/tmp/.productbrain/skills/test-skill.md',
|
|
314
|
+
};
|
|
315
|
+
const output = generateCodexSkill(skill);
|
|
316
|
+
expect(output).toContain('Codex-only content.');
|
|
317
|
+
expect(output).not.toContain('Claude-only content.');
|
|
318
|
+
expect(output).toContain('Universal content.');
|
|
319
|
+
});
|
|
320
|
+
it('generates a Codex skill index with links to projected skills', () => {
|
|
321
|
+
const skills = [
|
|
322
|
+
{
|
|
323
|
+
name: 'preflight',
|
|
324
|
+
description: 'Ground the task before changing code.',
|
|
325
|
+
triggers: ['preflight', 'system check'],
|
|
326
|
+
body: '# Preflight',
|
|
327
|
+
sourcePath: '/tmp/.productbrain/skills/preflight.md',
|
|
328
|
+
},
|
|
329
|
+
];
|
|
330
|
+
const output = generateCodexSkillIndex(skills);
|
|
331
|
+
expect(output).toContain('Product Brain Skills for Codex');
|
|
332
|
+
expect(output).toContain('## preflight');
|
|
333
|
+
expect(output).toContain('`preflight`');
|
|
334
|
+
expect(output).toContain('Read: `.codex/skills/preflight.md`');
|
|
335
|
+
});
|
|
336
|
+
it('hides learnings and template files from the Codex skill index', () => {
|
|
337
|
+
const skills = [
|
|
338
|
+
{
|
|
339
|
+
name: 'preflight',
|
|
340
|
+
description: 'Ground the task before changing code.',
|
|
341
|
+
triggers: ['preflight'],
|
|
342
|
+
body: '# Preflight',
|
|
343
|
+
sourcePath: '/tmp/.productbrain/skills/preflight.md',
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
name: 'retro-learnings',
|
|
347
|
+
description: 'Internal learnings companion.',
|
|
348
|
+
triggers: [],
|
|
349
|
+
body: '# Retro Learnings',
|
|
350
|
+
sourcePath: '/tmp/.productbrain/skills/retro-learnings.md',
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
name: 'LEARNINGS-TEMPLATE',
|
|
354
|
+
description: 'Template',
|
|
355
|
+
triggers: [],
|
|
356
|
+
body: '# Template',
|
|
357
|
+
sourcePath: '/tmp/.productbrain/skills/LEARNINGS-TEMPLATE.md',
|
|
358
|
+
},
|
|
359
|
+
];
|
|
360
|
+
const output = generateCodexSkillIndex(skills);
|
|
361
|
+
expect(output).toContain('## preflight');
|
|
362
|
+
expect(output).not.toContain('## retro-learnings');
|
|
363
|
+
expect(output).not.toContain('## LEARNINGS-TEMPLATE');
|
|
364
|
+
expect(output).toContain('hidden from this index');
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
describe('generateClaudeSkillRouter (target filtering)', () => {
|
|
368
|
+
it('includes only skills with matching or no targets', () => {
|
|
369
|
+
const claudeOnly = {
|
|
370
|
+
name: 'claude-skill',
|
|
371
|
+
description: 'Claude only',
|
|
372
|
+
triggers: ['test-claude'],
|
|
373
|
+
body: '# Claude',
|
|
374
|
+
sourcePath: '/tmp/skills/claude-skill.md',
|
|
375
|
+
targets: ['claude'],
|
|
376
|
+
};
|
|
377
|
+
const cursorOnly = {
|
|
378
|
+
name: 'cursor-skill',
|
|
379
|
+
description: 'Cursor only',
|
|
380
|
+
triggers: ['test-cursor'],
|
|
381
|
+
body: '# Cursor',
|
|
382
|
+
sourcePath: '/tmp/skills/cursor-skill.md',
|
|
383
|
+
targets: ['cursor'],
|
|
384
|
+
};
|
|
385
|
+
const universal = {
|
|
386
|
+
name: 'universal-skill',
|
|
387
|
+
description: 'Universal',
|
|
388
|
+
triggers: ['test-universal'],
|
|
389
|
+
body: '# Universal',
|
|
390
|
+
sourcePath: '/tmp/skills/universal-skill.md',
|
|
391
|
+
};
|
|
392
|
+
// Filter skills for Claude (as handshake.ts would do)
|
|
393
|
+
const claudeSkills = [claudeOnly, cursorOnly, universal].filter((s) => shouldEmitToTarget(s, 'claude'));
|
|
394
|
+
const router = generateClaudeSkillRouter(claudeSkills);
|
|
395
|
+
expect(router).toContain('claude-skill');
|
|
396
|
+
expect(router).toContain('universal-skill');
|
|
397
|
+
expect(router).not.toContain('cursor-skill');
|
|
398
|
+
});
|
|
399
|
+
it('skill with targets: [claude] does not appear in Cursor output', () => {
|
|
400
|
+
const claudeOnly = {
|
|
401
|
+
name: 'claude-exclusive',
|
|
402
|
+
description: 'Claude exclusive skill',
|
|
403
|
+
triggers: ['claude-trigger'],
|
|
404
|
+
body: '# Claude Exclusive',
|
|
405
|
+
sourcePath: '/tmp/skills/claude-exclusive.md',
|
|
406
|
+
targets: ['claude'],
|
|
407
|
+
};
|
|
408
|
+
// Filter for Cursor (as handshake.ts would do)
|
|
409
|
+
const cursorSkills = [claudeOnly].filter((s) => shouldEmitToTarget(s, 'cursor'));
|
|
410
|
+
expect(cursorSkills).toHaveLength(0);
|
|
411
|
+
});
|
|
412
|
+
it('skill with no targets appears in both Claude and Cursor output', () => {
|
|
413
|
+
const universal = {
|
|
414
|
+
name: 'universal',
|
|
415
|
+
description: 'Universal skill',
|
|
416
|
+
triggers: ['uni'],
|
|
417
|
+
body: '# Universal',
|
|
418
|
+
sourcePath: '/tmp/skills/universal.md',
|
|
419
|
+
};
|
|
420
|
+
const claudeSkills = [universal].filter((s) => shouldEmitToTarget(s, 'claude'));
|
|
421
|
+
const cursorSkills = [universal].filter((s) => shouldEmitToTarget(s, 'cursor'));
|
|
422
|
+
expect(claudeSkills).toHaveLength(1);
|
|
423
|
+
expect(cursorSkills).toHaveLength(1);
|
|
424
|
+
const router = generateClaudeSkillRouter(claudeSkills);
|
|
425
|
+
expect(router).toContain('universal');
|
|
426
|
+
const cursorOutput = generateCursorSkill(cursorSkills[0]);
|
|
427
|
+
expect(cursorOutput).toContain('universal');
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
describe('generateCursorRule (scope propagation)', () => {
|
|
431
|
+
const baseRule = {
|
|
432
|
+
name: 'test-rule',
|
|
433
|
+
description: 'A test rule',
|
|
434
|
+
autoApply: true,
|
|
435
|
+
body: '# Rule body content',
|
|
436
|
+
sourcePath: '/tmp/.productbrain/rules/test-rule.md',
|
|
437
|
+
};
|
|
438
|
+
it('emits empty globs and alwaysApply: true when scope is empty string', () => {
|
|
439
|
+
const rule = { ...baseRule, scope: '' };
|
|
440
|
+
const output = generateCursorRule(rule);
|
|
441
|
+
expect(output).toContain('globs: ');
|
|
442
|
+
expect(output).toContain('alwaysApply: true');
|
|
443
|
+
expect(output).toContain('# Rule body content');
|
|
444
|
+
});
|
|
445
|
+
it('emits empty globs and alwaysApply: true when scope is undefined', () => {
|
|
446
|
+
const rule = { ...baseRule };
|
|
447
|
+
const output = generateCursorRule(rule);
|
|
448
|
+
expect(output).toContain('globs: ');
|
|
449
|
+
expect(output).toContain('alwaysApply: true');
|
|
450
|
+
});
|
|
451
|
+
it('emits glob pattern and alwaysApply: false when scope is set', () => {
|
|
452
|
+
const rule = { ...baseRule, scope: 'convex/**/*.ts' };
|
|
453
|
+
const output = generateCursorRule(rule);
|
|
454
|
+
expect(output).toContain('globs: convex/**/*.ts');
|
|
455
|
+
expect(output).toContain('alwaysApply: false');
|
|
456
|
+
});
|
|
457
|
+
it('respects autoApply: false with empty scope', () => {
|
|
458
|
+
const rule = { ...baseRule, autoApply: false, scope: '' };
|
|
459
|
+
const output = generateCursorRule(rule);
|
|
460
|
+
expect(output).toContain('alwaysApply: false');
|
|
461
|
+
});
|
|
462
|
+
it('replaces .productbrain paths with .cursor paths in body', () => {
|
|
463
|
+
const rule = {
|
|
464
|
+
...baseRule,
|
|
465
|
+
body: 'Read `.productbrain/rules/foo.md` and `.productbrain/skills/bar.md`.',
|
|
466
|
+
};
|
|
467
|
+
const output = generateCursorRule(rule);
|
|
468
|
+
expect(output).toContain('.cursor/rules/foo.mdc');
|
|
469
|
+
expect(output).toContain('.cursor/skills/bar/SKILL.md');
|
|
470
|
+
expect(output).not.toContain('.productbrain/rules/foo.md');
|
|
471
|
+
});
|
|
472
|
+
it('includes source marker comment', () => {
|
|
473
|
+
const output = generateCursorRule(baseRule);
|
|
474
|
+
expect(output).toContain('source: .productbrain/rules/test-rule.md');
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
describe('generateClaudeRule (scope propagation)', () => {
|
|
478
|
+
const baseRule = {
|
|
479
|
+
name: 'test-rule',
|
|
480
|
+
description: 'A test rule',
|
|
481
|
+
autoApply: true,
|
|
482
|
+
body: '# Rule body content',
|
|
483
|
+
sourcePath: '/tmp/.productbrain/rules/test-rule.md',
|
|
484
|
+
};
|
|
485
|
+
it('omits paths when scope is empty string', () => {
|
|
486
|
+
const rule = { ...baseRule, scope: '' };
|
|
487
|
+
const output = generateClaudeRule(rule);
|
|
488
|
+
expect(output).not.toContain('paths:');
|
|
489
|
+
expect(output).toContain('description: "A test rule"');
|
|
490
|
+
expect(output).toContain('# Rule body content');
|
|
491
|
+
});
|
|
492
|
+
it('omits paths when scope is undefined', () => {
|
|
493
|
+
const rule = { ...baseRule };
|
|
494
|
+
const output = generateClaudeRule(rule);
|
|
495
|
+
expect(output).not.toContain('paths:');
|
|
496
|
+
});
|
|
497
|
+
it('emits paths array when scope is set', () => {
|
|
498
|
+
const rule = { ...baseRule, scope: 'convex/**/*.ts' };
|
|
499
|
+
const output = generateClaudeRule(rule);
|
|
500
|
+
expect(output).toContain('paths:');
|
|
501
|
+
expect(output).toContain(' - "convex/**/*.ts"');
|
|
502
|
+
});
|
|
503
|
+
it('escapes quotes in description', () => {
|
|
504
|
+
const rule = { ...baseRule, description: 'Rule with "quotes" inside' };
|
|
505
|
+
const output = generateClaudeRule(rule);
|
|
506
|
+
expect(output).toContain('description: "Rule with \\"quotes\\" inside"');
|
|
507
|
+
});
|
|
508
|
+
it('replaces .productbrain paths with .claude paths in body', () => {
|
|
509
|
+
const rule = {
|
|
510
|
+
...baseRule,
|
|
511
|
+
body: 'Read `.productbrain/rules/foo.md` for details.',
|
|
512
|
+
};
|
|
513
|
+
const output = generateClaudeRule(rule);
|
|
514
|
+
expect(output).toContain('.claude/rules/foo.md');
|
|
515
|
+
expect(output).not.toContain('.productbrain/rules/foo.md');
|
|
516
|
+
});
|
|
517
|
+
it('includes source marker comment', () => {
|
|
518
|
+
const output = generateClaudeRule(baseRule);
|
|
519
|
+
expect(output).toContain('source: .productbrain/rules/test-rule.md');
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
describe('generateCodexSkillIndex (empty primary skills)', () => {
|
|
523
|
+
it('shows empty message when all skills are learnings/templates', () => {
|
|
524
|
+
const skills = [
|
|
525
|
+
{
|
|
526
|
+
name: 'retro-learnings',
|
|
527
|
+
description: 'Internal learnings companion.',
|
|
528
|
+
triggers: [],
|
|
529
|
+
body: '# Retro Learnings',
|
|
530
|
+
sourcePath: '/tmp/.productbrain/skills/retro-learnings.md',
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
name: 'LEARNINGS-TEMPLATE',
|
|
534
|
+
description: 'Template',
|
|
535
|
+
triggers: [],
|
|
536
|
+
body: '# Template',
|
|
537
|
+
sourcePath: '/tmp/.productbrain/skills/LEARNINGS-TEMPLATE.md',
|
|
538
|
+
},
|
|
539
|
+
];
|
|
540
|
+
const output = generateCodexSkillIndex(skills);
|
|
541
|
+
expect(output).toContain('No Product Brain skills are currently projected');
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
describe('filterByLevel', () => {
|
|
545
|
+
const items = [
|
|
546
|
+
{ name: 'core-item', level: 'core' },
|
|
547
|
+
{ name: 'intermediate-item', level: 'intermediate' },
|
|
548
|
+
{ name: 'expert-item', level: 'expert' },
|
|
549
|
+
{ name: 'no-level-item' },
|
|
550
|
+
];
|
|
551
|
+
it('filterByLevel("beginner") returns only level:core items + items with no level', () => {
|
|
552
|
+
const result = filterByLevel(items, 'beginner');
|
|
553
|
+
const names = result.map((i) => i.name);
|
|
554
|
+
expect(names).toContain('core-item');
|
|
555
|
+
expect(names).toContain('no-level-item');
|
|
556
|
+
expect(names).not.toContain('intermediate-item');
|
|
557
|
+
expect(names).not.toContain('expert-item');
|
|
558
|
+
expect(result).toHaveLength(2);
|
|
559
|
+
});
|
|
560
|
+
it('filterByLevel("intermediate") returns core + intermediate + no level', () => {
|
|
561
|
+
const result = filterByLevel(items, 'intermediate');
|
|
562
|
+
const names = result.map((i) => i.name);
|
|
563
|
+
expect(names).toContain('core-item');
|
|
564
|
+
expect(names).toContain('intermediate-item');
|
|
565
|
+
expect(names).toContain('no-level-item');
|
|
566
|
+
expect(names).not.toContain('expert-item');
|
|
567
|
+
expect(result).toHaveLength(3);
|
|
568
|
+
});
|
|
569
|
+
it('filterByLevel("expert") returns all items', () => {
|
|
570
|
+
const result = filterByLevel(items, 'expert');
|
|
571
|
+
expect(result).toHaveLength(4);
|
|
572
|
+
});
|
|
573
|
+
it('filterByLevel(undefined) returns all items (backward compat)', () => {
|
|
574
|
+
const result = filterByLevel(items, undefined);
|
|
575
|
+
expect(result).toHaveLength(4);
|
|
576
|
+
});
|
|
577
|
+
it('filterByLevel(null-ish) returns all items (backward compat)', () => {
|
|
578
|
+
const result = filterByLevel(items);
|
|
579
|
+
expect(result).toHaveLength(4);
|
|
580
|
+
});
|
|
581
|
+
it('unknown level throws an error', () => {
|
|
582
|
+
expect(() => filterByLevel(items, 'unknown')).toThrow('Unknown level "unknown"');
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
describe('filterByLevel — stage-gating (DEC-443)', () => {
|
|
586
|
+
const items = [
|
|
587
|
+
{ name: 'core-item', level: 'core' },
|
|
588
|
+
{ name: 'intermediate-item', level: 'intermediate' },
|
|
589
|
+
{ name: 'expert-item', level: 'expert' },
|
|
590
|
+
{ name: 'no-level-item' },
|
|
591
|
+
];
|
|
592
|
+
it('stage=blank caps at core even if requestedLevel=expert', () => {
|
|
593
|
+
const result = filterByLevel(items, 'expert', 'blank');
|
|
594
|
+
const names = result.map((i) => i.name);
|
|
595
|
+
expect(names).toContain('core-item');
|
|
596
|
+
expect(names).toContain('no-level-item');
|
|
597
|
+
expect(names).not.toContain('intermediate-item');
|
|
598
|
+
expect(names).not.toContain('expert-item');
|
|
599
|
+
expect(result).toHaveLength(2);
|
|
600
|
+
});
|
|
601
|
+
it('stage=seed caps at intermediate (core + intermediate + no-level)', () => {
|
|
602
|
+
const result = filterByLevel(items, 'expert', 'seed');
|
|
603
|
+
const names = result.map((i) => i.name);
|
|
604
|
+
expect(names).toContain('core-item');
|
|
605
|
+
expect(names).toContain('intermediate-item');
|
|
606
|
+
expect(names).toContain('no-level-item');
|
|
607
|
+
expect(names).not.toContain('expert-item');
|
|
608
|
+
expect(result).toHaveLength(3);
|
|
609
|
+
});
|
|
610
|
+
it('stage=grounded applies no cap — all items returned', () => {
|
|
611
|
+
const result = filterByLevel(items, 'expert', 'grounded');
|
|
612
|
+
expect(result).toHaveLength(4);
|
|
613
|
+
});
|
|
614
|
+
it('stage=connected applies no cap — all items returned', () => {
|
|
615
|
+
const result = filterByLevel(items, 'expert', 'connected');
|
|
616
|
+
expect(result).toHaveLength(4);
|
|
617
|
+
});
|
|
618
|
+
it('stage=undefined (no profile) — no cap, backward compat', () => {
|
|
619
|
+
const result = filterByLevel(items, 'expert', undefined);
|
|
620
|
+
expect(result).toHaveLength(4);
|
|
621
|
+
});
|
|
622
|
+
it('unknown stage — fail-open, no cap applied', () => {
|
|
623
|
+
const result = filterByLevel(items, 'expert', 'unknown-stage');
|
|
624
|
+
expect(result).toHaveLength(4);
|
|
625
|
+
});
|
|
626
|
+
it('no requestedLevel + stage=blank still caps at beginner (core + no-level only)', () => {
|
|
627
|
+
const result = filterByLevel(items, undefined, 'blank');
|
|
628
|
+
const names = result.map((i) => i.name);
|
|
629
|
+
expect(names).toContain('core-item');
|
|
630
|
+
expect(names).toContain('no-level-item');
|
|
631
|
+
expect(names).not.toContain('intermediate-item');
|
|
632
|
+
expect(names).not.toContain('expert-item');
|
|
633
|
+
expect(result).toHaveLength(2);
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
describe('evaluateConditions', () => {
|
|
637
|
+
const noProfile = null;
|
|
638
|
+
const profile = {
|
|
639
|
+
stage: 'grounded',
|
|
640
|
+
totalRelations: 10,
|
|
641
|
+
};
|
|
642
|
+
const emptyRepo = { detectedStack: [] };
|
|
643
|
+
const sveltekitRepo = { detectedStack: ['sveltekit', 'typescript'] };
|
|
644
|
+
// 1. No conditions → included
|
|
645
|
+
it('no conditions → included', () => {
|
|
646
|
+
const result = evaluateConditions({}, profile, sveltekitRepo);
|
|
647
|
+
expect(result.included).toBe(true);
|
|
648
|
+
expect(result.reasons).toEqual(['no conditions']);
|
|
649
|
+
});
|
|
650
|
+
// 2. Matching when_stack → included
|
|
651
|
+
it('matching when_stack → included', () => {
|
|
652
|
+
const result = evaluateConditions({ when_stack: 'sveltekit' }, profile, sveltekitRepo);
|
|
653
|
+
expect(result.included).toBe(true);
|
|
654
|
+
expect(result.reasons.some((r) => r.includes('matched'))).toBe(true);
|
|
655
|
+
});
|
|
656
|
+
// 3. Non-matching when_stack → excluded with reason
|
|
657
|
+
it('non-matching when_stack → excluded with reason', () => {
|
|
658
|
+
const result = evaluateConditions({ when_stack: 'nextjs' }, profile, sveltekitRepo);
|
|
659
|
+
expect(result.included).toBe(false);
|
|
660
|
+
expect(result.reasons.some((r) => r.includes('when_stack=nextjs'))).toBe(true);
|
|
661
|
+
});
|
|
662
|
+
// 4. when_stack case-insensitive matching
|
|
663
|
+
it('when_stack is case-insensitive', () => {
|
|
664
|
+
const result = evaluateConditions({ when_stack: 'SvelteKit' }, profile, { detectedStack: ['sveltekit'] });
|
|
665
|
+
expect(result.included).toBe(true);
|
|
666
|
+
});
|
|
667
|
+
// 5. when_minStage ordering: blank < seed < grounded < connected < critical
|
|
668
|
+
it('when_minStage ordering: stage below threshold → excluded', () => {
|
|
669
|
+
const seedProfile = { stage: 'seed', totalRelations: 10 };
|
|
670
|
+
const result = evaluateConditions({ when_minStage: 'grounded' }, seedProfile, emptyRepo);
|
|
671
|
+
expect(result.included).toBe(false);
|
|
672
|
+
expect(result.reasons.some((r) => r.includes('when_minStage=grounded'))).toBe(true);
|
|
673
|
+
});
|
|
674
|
+
// 6. when_minStage with stage below threshold → excluded
|
|
675
|
+
it('when_minStage blank < connected threshold → excluded', () => {
|
|
676
|
+
const blankProfile = { stage: 'blank', totalRelations: 0 };
|
|
677
|
+
const result = evaluateConditions({ when_minStage: 'connected' }, blankProfile, emptyRepo);
|
|
678
|
+
expect(result.included).toBe(false);
|
|
679
|
+
});
|
|
680
|
+
// 7. when_minStage with stage at threshold → included
|
|
681
|
+
it('when_minStage with stage exactly at threshold → included', () => {
|
|
682
|
+
const result = evaluateConditions({ when_minStage: 'grounded' }, profile, emptyRepo);
|
|
683
|
+
expect(result.included).toBe(true);
|
|
684
|
+
expect(result.reasons.some((r) => r.includes('satisfied'))).toBe(true);
|
|
685
|
+
});
|
|
686
|
+
// Also verify stages above threshold pass
|
|
687
|
+
it('when_minStage with stage above threshold → included', () => {
|
|
688
|
+
const criticalProfile = { stage: 'critical', totalRelations: 10 };
|
|
689
|
+
const result = evaluateConditions({ when_minStage: 'grounded' }, criticalProfile, emptyRepo);
|
|
690
|
+
expect(result.included).toBe(true);
|
|
691
|
+
});
|
|
692
|
+
// 8. when_minGovernance with enough relations → included
|
|
693
|
+
it('when_minGovernance with enough relations → included', () => {
|
|
694
|
+
const result = evaluateConditions({ when_minGovernance: '5' }, profile, emptyRepo);
|
|
695
|
+
expect(result.included).toBe(true);
|
|
696
|
+
expect(result.reasons.some((r) => r.includes('satisfied'))).toBe(true);
|
|
697
|
+
});
|
|
698
|
+
// 9. when_minGovernance with too few → excluded
|
|
699
|
+
it('when_minGovernance with too few relations → excluded', () => {
|
|
700
|
+
const lowRelProfile = { stage: 'grounded', totalRelations: 3 };
|
|
701
|
+
const result = evaluateConditions({ when_minGovernance: '5' }, lowRelProfile, emptyRepo);
|
|
702
|
+
expect(result.included).toBe(false);
|
|
703
|
+
expect(result.reasons.some((r) => r.includes('when_minGovernance=5'))).toBe(true);
|
|
704
|
+
});
|
|
705
|
+
// 10. Multiple conditions: all pass → included
|
|
706
|
+
it('multiple conditions: all pass → included', () => {
|
|
707
|
+
const result = evaluateConditions({ when_stack: 'sveltekit', when_minStage: 'seed', when_minGovernance: '5' }, profile, sveltekitRepo);
|
|
708
|
+
expect(result.included).toBe(true);
|
|
709
|
+
});
|
|
710
|
+
// 11. Multiple conditions: one fails → excluded (AND semantics)
|
|
711
|
+
it('multiple conditions: one fails → excluded', () => {
|
|
712
|
+
const result = evaluateConditions({ when_stack: 'sveltekit', when_minStage: 'critical' }, profile, // stage=grounded, below critical
|
|
713
|
+
sveltekitRepo);
|
|
714
|
+
expect(result.included).toBe(false);
|
|
715
|
+
expect(result.reasons.some((r) => r.includes('when_minStage=critical'))).toBe(true);
|
|
716
|
+
});
|
|
717
|
+
// 12. Null profile + when_minStage → included (fail-open)
|
|
718
|
+
it('null profile with when_minStage → fail-open (included)', () => {
|
|
719
|
+
const result = evaluateConditions({ when_minStage: 'grounded' }, noProfile, emptyRepo);
|
|
720
|
+
expect(result.included).toBe(true);
|
|
721
|
+
expect(result.reasons.some((r) => r.includes('fail-open'))).toBe(true);
|
|
722
|
+
});
|
|
723
|
+
// 13. Null profile + when_stack only → still evaluated (stack comes from repoContext)
|
|
724
|
+
it('null profile with when_stack only → still evaluated from repoContext', () => {
|
|
725
|
+
const matchResult = evaluateConditions({ when_stack: 'sveltekit' }, noProfile, sveltekitRepo);
|
|
726
|
+
expect(matchResult.included).toBe(true);
|
|
727
|
+
const noMatchResult = evaluateConditions({ when_stack: 'nextjs' }, noProfile, sveltekitRepo);
|
|
728
|
+
expect(noMatchResult.included).toBe(false);
|
|
729
|
+
});
|
|
730
|
+
});
|
|
731
|
+
describe('BET-170 acceptance criteria — integration', () => {
|
|
732
|
+
/**
|
|
733
|
+
* Mock rules cover all combinations: stack condition, stage condition,
|
|
734
|
+
* governance condition, and unconditional at core/intermediate/expert levels.
|
|
735
|
+
*/
|
|
736
|
+
const mockRules = [
|
|
737
|
+
{ name: 'deployment', level: 'core', conditions: { when_stack: 'sveltekit' } },
|
|
738
|
+
{ name: 'feature-flags', level: 'intermediate', conditions: { when_minStage: 'grounded' } },
|
|
739
|
+
{ name: 'domain-boundaries', level: 'expert', conditions: { when_minGovernance: '5' } },
|
|
740
|
+
{ name: 'code-integrity', level: 'core' }, // no conditions — always included
|
|
741
|
+
{ name: 'review-gate', level: 'intermediate' }, // no conditions
|
|
742
|
+
{ name: 'orchestrator-mode', level: 'expert' }, // no conditions
|
|
743
|
+
];
|
|
744
|
+
function applyPipeline(rules, profile, repo) {
|
|
745
|
+
const profileStage = profile?.stage;
|
|
746
|
+
// stage-gated level filter: no requestedLevel (defaults to widest), stage caps it
|
|
747
|
+
const levelFiltered = filterByLevel(rules, undefined, profileStage);
|
|
748
|
+
// condition filter
|
|
749
|
+
return levelFiltered.filter((rule) => {
|
|
750
|
+
const result = evaluateConditions(rule.conditions ?? {}, profile, repo);
|
|
751
|
+
return result.included;
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
// AC1: Fresh Python workspace (stage=blank, 0 entries) — only core items with no conditions
|
|
755
|
+
it('AC1: stage=blank, python — only core unconditional items', () => {
|
|
756
|
+
const profile = { stage: 'blank', totalEntries: 0, totalRelations: 0 };
|
|
757
|
+
const repo = { detectedStack: ['python'] };
|
|
758
|
+
const result = applyPipeline(mockRules, profile, repo);
|
|
759
|
+
const names = result.map((r) => r.name);
|
|
760
|
+
// Only code-integrity: core level, no conditions
|
|
761
|
+
// deployment: core but when_stack=sveltekit fails (python != sveltekit)
|
|
762
|
+
expect(names).toContain('code-integrity');
|
|
763
|
+
expect(names).not.toContain('deployment'); // core but sveltekit condition fails
|
|
764
|
+
expect(names).not.toContain('feature-flags'); // intermediate — capped by blank stage
|
|
765
|
+
expect(names).not.toContain('domain-boundaries'); // expert — capped by blank stage
|
|
766
|
+
expect(names).not.toContain('review-gate'); // intermediate — capped by blank stage
|
|
767
|
+
expect(names).not.toContain('orchestrator-mode'); // expert — capped by blank stage
|
|
768
|
+
expect(result).toHaveLength(1);
|
|
769
|
+
});
|
|
770
|
+
// AC2: Node/TS workspace (stage=grounded, 30 entries) — stack-matched, all levels
|
|
771
|
+
it('AC2: stage=grounded, sveltekit/typescript, 30 entries — all rules included', () => {
|
|
772
|
+
const profile = { stage: 'grounded', totalEntries: 30, totalRelations: 15 };
|
|
773
|
+
const repo = { detectedStack: ['typescript', 'sveltekit'] };
|
|
774
|
+
const result = applyPipeline(mockRules, profile, repo);
|
|
775
|
+
const names = result.map((r) => r.name);
|
|
776
|
+
// All conditions pass: sveltekit matches, grounded >= grounded, 15 >= 5
|
|
777
|
+
expect(names).toContain('deployment');
|
|
778
|
+
expect(names).toContain('feature-flags');
|
|
779
|
+
expect(names).toContain('domain-boundaries');
|
|
780
|
+
expect(names).toContain('code-integrity');
|
|
781
|
+
expect(names).toContain('review-gate');
|
|
782
|
+
expect(names).toContain('orchestrator-mode');
|
|
783
|
+
expect(result).toHaveLength(6);
|
|
784
|
+
});
|
|
785
|
+
// AC3: Mature workspace (stage=connected, 100+ entries) — all rules and skills
|
|
786
|
+
it('AC3: stage=connected, 150 entries — all rules projected without filtering', () => {
|
|
787
|
+
const profile = { stage: 'connected', totalEntries: 150, totalRelations: 80 };
|
|
788
|
+
const repo = { detectedStack: ['typescript', 'sveltekit'] };
|
|
789
|
+
const result = applyPipeline(mockRules, profile, repo);
|
|
790
|
+
expect(result).toHaveLength(6);
|
|
791
|
+
});
|
|
792
|
+
// AC4: Unknown stack — general template set, no stack-specific rules
|
|
793
|
+
it('AC4: stage=grounded, unknown stack — only non-stack-conditional rules', () => {
|
|
794
|
+
const profile = { stage: 'grounded', totalEntries: 30, totalRelations: 15 };
|
|
795
|
+
const repo = { detectedStack: [] };
|
|
796
|
+
const result = applyPipeline(mockRules, profile, repo);
|
|
797
|
+
const names = result.map((r) => r.name);
|
|
798
|
+
// deployment requires sveltekit stack — excluded
|
|
799
|
+
// all others: feature-flags (grounded ok), domain-boundaries (15 >= 5), unconditionals all pass
|
|
800
|
+
expect(names).not.toContain('deployment');
|
|
801
|
+
expect(names).toContain('feature-flags');
|
|
802
|
+
expect(names).toContain('domain-boundaries');
|
|
803
|
+
expect(names).toContain('code-integrity');
|
|
804
|
+
expect(names).toContain('review-gate');
|
|
805
|
+
expect(names).toContain('orchestrator-mode');
|
|
806
|
+
expect(result).toHaveLength(5);
|
|
807
|
+
});
|
|
808
|
+
// AC5: workspaceReadiness failure (null profile) — fail-open, all rules/skills project
|
|
809
|
+
it('AC5: null profile (readiness failure) — fail-open, profile conditions pass', () => {
|
|
810
|
+
const profile = null;
|
|
811
|
+
const repo = { detectedStack: ['typescript'] };
|
|
812
|
+
const result = applyPipeline(mockRules, profile, repo);
|
|
813
|
+
const names = result.map((r) => r.name);
|
|
814
|
+
// Null profile:
|
|
815
|
+
// - filterByLevel: no stage cap → all levels pass
|
|
816
|
+
// - evaluateConditions: when_minStage + when_minGovernance → fail-open (included)
|
|
817
|
+
// - when_stack: still evaluated against repoContext; sveltekit not in [typescript] → excluded
|
|
818
|
+
expect(names).not.toContain('deployment'); // when_stack=sveltekit, typescript doesn't match
|
|
819
|
+
expect(names).toContain('feature-flags'); // fail-open (profile null)
|
|
820
|
+
expect(names).toContain('domain-boundaries'); // fail-open (profile null)
|
|
821
|
+
expect(names).toContain('code-integrity');
|
|
822
|
+
expect(names).toContain('review-gate');
|
|
823
|
+
expect(names).toContain('orchestrator-mode');
|
|
824
|
+
expect(result).toHaveLength(5);
|
|
825
|
+
});
|
|
826
|
+
// Scale test: 0/5/50/500 entries — filtering correctness at different entry counts
|
|
827
|
+
it('scale test: filtering correctness at 0/5/50/500 totalEntries', () => {
|
|
828
|
+
const repo = { detectedStack: ['sveltekit'] };
|
|
829
|
+
// 0 entries: blank stage, low relations
|
|
830
|
+
// deployment: core level (passes stage cap), sveltekit matches → included
|
|
831
|
+
// code-integrity: core level, no conditions → included
|
|
832
|
+
// everything else: capped by blank stage (intermediate/expert excluded)
|
|
833
|
+
const result0 = applyPipeline(mockRules, { stage: 'blank', totalEntries: 0, totalRelations: 0 }, repo);
|
|
834
|
+
const names0 = result0.map((r) => r.name);
|
|
835
|
+
expect(names0).toContain('deployment');
|
|
836
|
+
expect(names0).toContain('code-integrity');
|
|
837
|
+
expect(names0).not.toContain('feature-flags'); // intermediate — capped
|
|
838
|
+
expect(names0).not.toContain('domain-boundaries'); // expert — capped
|
|
839
|
+
expect(names0).not.toContain('review-gate'); // intermediate — capped
|
|
840
|
+
expect(names0).not.toContain('orchestrator-mode'); // expert — capped
|
|
841
|
+
expect(result0).toHaveLength(2);
|
|
842
|
+
// 5 entries: seed stage, 2 relations (below governance threshold)
|
|
843
|
+
const result5 = applyPipeline(mockRules, { stage: 'seed', totalEntries: 5, totalRelations: 2 }, repo);
|
|
844
|
+
const names5 = result5.map((r) => r.name);
|
|
845
|
+
// seed caps at intermediate; domain-boundaries (expert) + orchestrator-mode (expert) excluded
|
|
846
|
+
// domain-boundaries also fails governance (2 < 5)
|
|
847
|
+
// feature-flags: intermediate ok, but when_minStage=grounded fails for seed → excluded
|
|
848
|
+
// deployment: core ok, sveltekit matches → included
|
|
849
|
+
expect(names5).toContain('deployment');
|
|
850
|
+
expect(names5).toContain('code-integrity');
|
|
851
|
+
expect(names5).toContain('review-gate');
|
|
852
|
+
expect(names5).not.toContain('domain-boundaries');
|
|
853
|
+
expect(names5).not.toContain('orchestrator-mode');
|
|
854
|
+
expect(names5).not.toContain('feature-flags');
|
|
855
|
+
// 50 entries: grounded stage, 20 relations
|
|
856
|
+
const result50 = applyPipeline(mockRules, { stage: 'grounded', totalEntries: 50, totalRelations: 20 }, repo);
|
|
857
|
+
expect(result50).toHaveLength(6);
|
|
858
|
+
// 500 entries: connected stage, 200 relations
|
|
859
|
+
const result500 = applyPipeline(mockRules, { stage: 'connected', totalEntries: 500, totalRelations: 200 }, repo);
|
|
860
|
+
expect(result500).toHaveLength(6);
|
|
861
|
+
});
|
|
862
|
+
});
|
|
863
|
+
describe('readCanonicalRules — backward compat with when_* frontmatter', () => {
|
|
864
|
+
beforeEach(() => {
|
|
865
|
+
Object.keys(vfs).forEach((k) => delete vfs[k]);
|
|
866
|
+
});
|
|
867
|
+
it('parses when_stack from frontmatter into conditions', () => {
|
|
868
|
+
vfs[join(PB_DIR, 'rules', 'deploy.md')] = `---
|
|
869
|
+
name: deploy
|
|
870
|
+
description: Deploy rule
|
|
871
|
+
scope: ""
|
|
872
|
+
autoApply: true
|
|
873
|
+
when_stack: sveltekit
|
|
874
|
+
---
|
|
875
|
+
|
|
876
|
+
# Deploy body
|
|
877
|
+
`;
|
|
878
|
+
const rules = readCanonicalRules(PB_DIR);
|
|
879
|
+
expect(rules).toHaveLength(1);
|
|
880
|
+
expect(rules[0].conditions?.when_stack).toBe('sveltekit');
|
|
881
|
+
});
|
|
882
|
+
it('parses when_minStage from frontmatter into conditions', () => {
|
|
883
|
+
vfs[join(PB_DIR, 'rules', 'flags.md')] = `---
|
|
884
|
+
name: flags
|
|
885
|
+
description: Feature flags rule
|
|
886
|
+
scope: ""
|
|
887
|
+
autoApply: false
|
|
888
|
+
when_minStage: grounded
|
|
889
|
+
---
|
|
890
|
+
|
|
891
|
+
# Flags body
|
|
892
|
+
`;
|
|
893
|
+
const rules = readCanonicalRules(PB_DIR);
|
|
894
|
+
expect(rules).toHaveLength(1);
|
|
895
|
+
expect(rules[0].conditions?.when_minStage).toBe('grounded');
|
|
896
|
+
});
|
|
897
|
+
it('parses when_minGovernance from frontmatter into conditions', () => {
|
|
898
|
+
vfs[join(PB_DIR, 'rules', 'domains.md')] = `---
|
|
899
|
+
name: domains
|
|
900
|
+
description: Domain boundaries rule
|
|
901
|
+
scope: ""
|
|
902
|
+
autoApply: true
|
|
903
|
+
when_minGovernance: 5
|
|
904
|
+
---
|
|
905
|
+
|
|
906
|
+
# Domains body
|
|
907
|
+
`;
|
|
908
|
+
const rules = readCanonicalRules(PB_DIR);
|
|
909
|
+
expect(rules).toHaveLength(1);
|
|
910
|
+
expect(rules[0].conditions?.when_minGovernance).toBe('5');
|
|
911
|
+
});
|
|
912
|
+
it('returns undefined conditions when no when_* keys are present (backward compat)', () => {
|
|
913
|
+
vfs[join(PB_DIR, 'rules', 'plain.md')] = `---
|
|
914
|
+
name: plain
|
|
915
|
+
description: Plain rule
|
|
916
|
+
scope: ""
|
|
917
|
+
autoApply: true
|
|
918
|
+
---
|
|
919
|
+
|
|
920
|
+
# Plain body
|
|
921
|
+
`;
|
|
922
|
+
const rules = readCanonicalRules(PB_DIR);
|
|
923
|
+
expect(rules).toHaveLength(1);
|
|
924
|
+
expect(rules[0].conditions).toBeUndefined();
|
|
925
|
+
});
|
|
926
|
+
});
|
|
927
|
+
//# sourceMappingURL=portable-knowledge.test.js.map
|