@svadmin/create 0.40.3 → 0.41.1

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/dist/index.js CHANGED
@@ -17670,7 +17670,7 @@ function buildAdminAiManifest(options) {
17670
17670
  notes: SCAFFOLD_MIGRATION_NOTES
17671
17671
  },
17672
17672
  plugins: [],
17673
- guidance: ["AGENTS.md", "DESIGN.md"]
17673
+ guidance: ["AGENTS.md", "DESIGN.md", "PRODUCT.md"]
17674
17674
  };
17675
17675
  }
17676
17676
  function buildScaffoldPlatformFiles(options) {
@@ -18685,6 +18685,488 @@ var resources = [
18685
18685
 
18686
18686
  // src/vibe-command.ts
18687
18687
  init_vibe_catalog();
18688
+
18689
+ // ../core/src/product-plan.ts
18690
+ var PRODUCT_PLAN_SCHEMA_VERSION = "product/v1";
18691
+ function isRecord2(value) {
18692
+ return typeof value === "object" && value !== null && !Array.isArray(value);
18693
+ }
18694
+ function hasStringId(value) {
18695
+ return isRecord2(value) && typeof value["id"] === "string" && value["id"].trim().length > 0;
18696
+ }
18697
+ var PRODUCT_DISCLOSURES = ["visible", "collapsed", "more", "context", "hidden"];
18698
+ var PRODUCT_PRIORITIES = ["core", "secondary", "future"];
18699
+ var PRODUCT_FREQUENCIES = ["high", "medium", "low", "unknown"];
18700
+ var PRODUCT_PLACEMENTS = ["primary", "secondary", "settings", "context", "command", "hidden"];
18701
+ var PRODUCT_PAGE_KINDS = ["workbench", "list", "detail", "form", "approval", "import", "settings", "custom"];
18702
+ var PRODUCT_STATES = ["empty", "loading", "error", "forbidden", "success"];
18703
+ function pushEnumIssue(issues, value, path, allowed) {
18704
+ if (typeof value !== "string" || !allowed.includes(value)) {
18705
+ issues.push({ code: "invalid-enum", path, message: path + " must be one of " + allowed.join(", ") + "." });
18706
+ }
18707
+ }
18708
+ function pushProductFieldIssues(issues, value, path) {
18709
+ if (!Array.isArray(value)) {
18710
+ issues.push({ code: "missing-array", path, message: path + " must be an array." });
18711
+ return;
18712
+ }
18713
+ const seen = new Set;
18714
+ value.forEach((item, index) => {
18715
+ const itemPath = path + "." + index;
18716
+ if (!isRecord2(item)) {
18717
+ issues.push({ code: "invalid-field", path: itemPath, message: itemPath + " must be an object." });
18718
+ return;
18719
+ }
18720
+ const fieldId = item["id"];
18721
+ if (typeof fieldId !== "string" || fieldId.trim().length === 0) {
18722
+ issues.push({ code: "invalid-id", path: itemPath + ".id", message: "Every product field needs a non-empty string id." });
18723
+ return;
18724
+ }
18725
+ if (seen.has(fieldId)) {
18726
+ issues.push({ code: "duplicate-id", path: itemPath + ".id", message: "Product field id " + fieldId + " is duplicated within its resource." });
18727
+ }
18728
+ seen.add(fieldId);
18729
+ for (const key of ["label", "type"]) {
18730
+ if (typeof item[key] !== "string")
18731
+ issues.push({ code: "invalid-field", path: itemPath + "." + key, message: itemPath + "." + key + " must be a string." });
18732
+ }
18733
+ for (const key of ["readableBy", "writableBy", "usedByWorkflowIds"]) {
18734
+ const candidate = item[key];
18735
+ if (!Array.isArray(candidate) || !candidate.every((entry) => typeof entry === "string")) {
18736
+ issues.push({ code: "invalid-field", path: itemPath + "." + key, message: itemPath + "." + key + " must be an array of strings." });
18737
+ }
18738
+ }
18739
+ for (const key of ["required", "sensitive"]) {
18740
+ if (item[key] !== undefined && typeof item[key] !== "boolean") {
18741
+ issues.push({ code: "invalid-field", path: itemPath + "." + key, message: itemPath + "." + key + " must be a boolean when provided." });
18742
+ }
18743
+ }
18744
+ if (item["disclosure"] !== undefined)
18745
+ pushEnumIssue(issues, item["disclosure"], itemPath + ".disclosure", PRODUCT_DISCLOSURES);
18746
+ });
18747
+ }
18748
+ function pushEntityIssues(issues, value, path, seen, shape = {}) {
18749
+ if (!Array.isArray(value)) {
18750
+ issues.push({ code: "missing-array", path, message: `${path} must be an array.` });
18751
+ return;
18752
+ }
18753
+ value.forEach((item, index) => {
18754
+ const itemPath = `${path}.${index}`;
18755
+ if (!hasStringId(item)) {
18756
+ issues.push({ code: "invalid-id", path: `${itemPath}.id`, message: "Every product entity needs a non-empty string id." });
18757
+ return;
18758
+ }
18759
+ if (seen.has(item.id)) {
18760
+ issues.push({ code: "duplicate-id", path: `${itemPath}.id`, message: `Product id "${item.id}" is duplicated.` });
18761
+ }
18762
+ seen.add(item.id);
18763
+ const record = isRecord2(item) ? item : undefined;
18764
+ if (!record)
18765
+ return;
18766
+ for (const key of shape.strings ?? []) {
18767
+ if (typeof record[key] !== "string")
18768
+ issues.push({ code: "invalid-field", path: `${itemPath}.${key}`, message: `${itemPath}.${key} must be a string.` });
18769
+ }
18770
+ for (const key of shape.arrays ?? []) {
18771
+ const candidate = record[key];
18772
+ if (!Array.isArray(candidate) || !candidate.every((value) => typeof value === "string")) {
18773
+ issues.push({ code: "invalid-field", path: `${itemPath}.${key}`, message: `${itemPath}.${key} must be an array of strings.` });
18774
+ }
18775
+ }
18776
+ });
18777
+ }
18778
+ function validateProductPlan(input) {
18779
+ const issues = [];
18780
+ if (!isRecord2(input))
18781
+ return { ok: false, issues: [{ code: "invalid-root", path: "", message: "Product plan must be a JSON object." }] };
18782
+ if (input["schemaVersion"] !== PRODUCT_PLAN_SCHEMA_VERSION) {
18783
+ issues.push({ code: "invalid-version", path: "schemaVersion", message: `Expected ${PRODUCT_PLAN_SCHEMA_VERSION}.` });
18784
+ }
18785
+ if (!isRecord2(input["brief"])) {
18786
+ issues.push({ code: "invalid-root", path: "brief", message: "Product plan brief must be an object." });
18787
+ } else {
18788
+ const brief = input["brief"];
18789
+ for (const key of ["id", "name", "description", "primaryOutcome", "primaryAudience"]) {
18790
+ if (typeof brief[key] !== "string")
18791
+ issues.push({ code: "invalid-field", path: `brief.${key}`, message: `brief.${key} must be a string.` });
18792
+ }
18793
+ for (const key of ["constraints", "nonGoals", "assumptions"]) {
18794
+ const candidate = brief[key];
18795
+ if (!Array.isArray(candidate) || !candidate.every((value) => typeof value === "string"))
18796
+ issues.push({ code: "invalid-field", path: `brief.${key}`, message: `brief.${key} must be an array of strings.` });
18797
+ }
18798
+ }
18799
+ const shapes = {
18800
+ roles: { strings: ["name"], arrays: ["permissions"] },
18801
+ workflows: { strings: ["name", "trigger", "goal", "priority", "frequency"], arrays: ["actorRoleIds", "steps", "successCriteria", "exceptions"] },
18802
+ resources: { strings: ["name", "label"], arrays: ["roleIds", "workflowIds"] },
18803
+ actions: { strings: ["label", "workflowId"], arrays: ["roleIds"] },
18804
+ surfaces: { strings: ["name", "kind", "purpose"], arrays: ["workflowIds", "resourceIds", "roleIds", "actionIds", "sections", "states"] },
18805
+ menu: { strings: ["label", "placement"], arrays: ["workflowIds", "roleIds"] }
18806
+ };
18807
+ for (const key of ["roles", "workflows", "resources", "actions", "surfaces", "menu"]) {
18808
+ pushEntityIssues(issues, input[key], key, new Set, shapes[key]);
18809
+ }
18810
+ const workflows = input["workflows"];
18811
+ if (Array.isArray(workflows))
18812
+ workflows.forEach((workflow, index) => {
18813
+ if (!isRecord2(workflow))
18814
+ return;
18815
+ if (typeof workflow["priority"] === "string")
18816
+ pushEnumIssue(issues, workflow["priority"], "workflows." + index + ".priority", PRODUCT_PRIORITIES);
18817
+ if (typeof workflow["frequency"] === "string")
18818
+ pushEnumIssue(issues, workflow["frequency"], "workflows." + index + ".frequency", PRODUCT_FREQUENCIES);
18819
+ });
18820
+ const resources = input["resources"];
18821
+ if (Array.isArray(resources))
18822
+ resources.forEach((resource, index) => {
18823
+ if (!isRecord2(resource))
18824
+ return;
18825
+ pushProductFieldIssues(issues, resource["fields"], "resources." + index + ".fields");
18826
+ if (resource["menuPlacement"] !== undefined)
18827
+ pushEnumIssue(issues, resource["menuPlacement"], "resources." + index + ".menuPlacement", PRODUCT_PLACEMENTS);
18828
+ if (resource["internal"] !== undefined && typeof resource["internal"] !== "boolean") {
18829
+ issues.push({ code: "invalid-field", path: "resources." + index + ".internal", message: "resources." + index + ".internal must be a boolean when provided." });
18830
+ }
18831
+ });
18832
+ const actions = input["actions"];
18833
+ if (Array.isArray(actions))
18834
+ actions.forEach((action, index) => {
18835
+ if (!isRecord2(action))
18836
+ return;
18837
+ if (action["disclosure"] !== undefined)
18838
+ pushEnumIssue(issues, action["disclosure"], "actions." + index + ".disclosure", PRODUCT_DISCLOSURES);
18839
+ if (action["destructive"] !== undefined && typeof action["destructive"] !== "boolean") {
18840
+ issues.push({ code: "invalid-field", path: "actions." + index + ".destructive", message: "actions." + index + ".destructive must be a boolean when provided." });
18841
+ }
18842
+ });
18843
+ const surfaces = input["surfaces"];
18844
+ if (Array.isArray(surfaces))
18845
+ surfaces.forEach((surface, index) => {
18846
+ if (!isRecord2(surface))
18847
+ return;
18848
+ if (typeof surface["kind"] === "string")
18849
+ pushEnumIssue(issues, surface["kind"], "surfaces." + index + ".kind", PRODUCT_PAGE_KINDS);
18850
+ if (surface["menuPlacement"] !== undefined)
18851
+ pushEnumIssue(issues, surface["menuPlacement"], "surfaces." + index + ".menuPlacement", PRODUCT_PLACEMENTS);
18852
+ if (surface["route"] !== undefined && (typeof surface["route"] !== "string" || surface["route"].trim().length === 0)) {
18853
+ issues.push({ code: "invalid-field", path: "surfaces." + index + ".route", message: "surfaces." + index + ".route must be a non-empty string when provided." });
18854
+ }
18855
+ const states = surface["states"];
18856
+ if (Array.isArray(states))
18857
+ states.forEach((state, stateIndex) => {
18858
+ if (typeof state === "string")
18859
+ pushEnumIssue(issues, state, "surfaces." + index + ".states." + stateIndex, PRODUCT_STATES);
18860
+ });
18861
+ });
18862
+ const menu = input["menu"];
18863
+ if (Array.isArray(menu))
18864
+ menu.forEach((entry, index) => {
18865
+ if (!isRecord2(entry))
18866
+ return;
18867
+ if (typeof entry["placement"] === "string")
18868
+ pushEnumIssue(issues, entry["placement"], "menu." + index + ".placement", PRODUCT_PLACEMENTS);
18869
+ for (const key of ["href", "parentId", "surfaceId", "resourceId", "condition"]) {
18870
+ if (entry[key] !== undefined && typeof entry[key] !== "string") {
18871
+ issues.push({ code: "invalid-field", path: "menu." + index + "." + key, message: "menu." + index + "." + key + " must be a string when provided." });
18872
+ }
18873
+ }
18874
+ if (entry["order"] !== undefined && (typeof entry["order"] !== "number" || !Number.isFinite(entry["order"]))) {
18875
+ issues.push({ code: "invalid-field", path: "menu." + index + ".order", message: "menu." + index + ".order must be a finite number when provided." });
18876
+ }
18877
+ });
18878
+ return issues.length === 0 ? { ok: true, value: input } : { ok: false, issues };
18879
+ }
18880
+
18881
+ class ProductPlanValidationError extends Error {
18882
+ issues;
18883
+ constructor(issues) {
18884
+ super(`Invalid product plan (${issues.length} issue${issues.length === 1 ? "" : "s"})`);
18885
+ this.name = "ProductPlanValidationError";
18886
+ this.issues = issues;
18887
+ }
18888
+ }
18889
+ function parseProductPlan(input) {
18890
+ const result = validateProductPlan(input);
18891
+ if (!result.ok)
18892
+ throw new ProductPlanValidationError(result.issues);
18893
+ return result.value;
18894
+ }
18895
+
18896
+ // ../core/src/product-navigation.ts
18897
+ function canSeeByRole(item, roleIds) {
18898
+ return item.roleIds.length === 0 || item.roleIds.some((roleId) => roleIds.includes(roleId));
18899
+ }
18900
+ function conditionEnabled(condition, flags) {
18901
+ return condition === undefined ? true : flags?.[condition] === true;
18902
+ }
18903
+ function createNode(item, allItems, context, ancestry = new Set) {
18904
+ const visibleByRole = canSeeByRole(item, context.roleIds);
18905
+ const visibleByCondition = conditionEnabled(item.condition, context.featureFlags);
18906
+ const visible = visibleByRole && visibleByCondition && item.placement !== "hidden";
18907
+ const nextAncestry = new Set(ancestry);
18908
+ nextAncestry.add(item.id);
18909
+ const children = nextAncestry.has(item.id) ? allItems.filter((candidate) => candidate.parentId === item.id && !nextAncestry.has(candidate.id)).sort((left, right) => (left.order ?? 0) - (right.order ?? 0)).map((candidate) => createNode(candidate, allItems, context, nextAncestry)) : [];
18910
+ return {
18911
+ ...item,
18912
+ children,
18913
+ visible,
18914
+ ...visible ? {} : { reason: visibleByRole ? "hidden by business condition or placement" : "current role is not allowed to see this entry" }
18915
+ };
18916
+ }
18917
+ function buildProductNavigation(plan, context) {
18918
+ return plan.menu.filter((item) => item.parentId === undefined).sort((left, right) => (left.order ?? 0) - (right.order ?? 0)).map((item) => createNode(item, plan.menu, context));
18919
+ }
18920
+
18921
+ // ../core/src/product-pages.ts
18922
+ function buildProductPagePlan(surface, plan) {
18923
+ const actionById = new Map(plan.actions.map((action) => [action.id, action]));
18924
+ const primaryAction = surface.primaryActionId === undefined ? undefined : actionById.get(surface.primaryActionId);
18925
+ const secondaryActions = surface.actionIds.filter((id) => id !== surface.primaryActionId).map((id) => actionById.get(id)).filter((action) => action !== undefined);
18926
+ const sections = surface.sections.length ? surface.sections.map((title, index) => ({
18927
+ id: `${surface.id}.section.${index + 1}`,
18928
+ title,
18929
+ resourceIds: index === 0 ? surface.resourceIds : []
18930
+ })) : [{ id: `${surface.id}.content`, title: surface.name, resourceIds: surface.resourceIds }];
18931
+ return {
18932
+ id: surface.id,
18933
+ kind: surface.kind,
18934
+ title: surface.name,
18935
+ purpose: surface.purpose,
18936
+ workflowIds: surface.workflowIds,
18937
+ roleIds: surface.roleIds,
18938
+ ...primaryAction === undefined ? {} : { primaryAction },
18939
+ secondaryActions,
18940
+ sections,
18941
+ states: surface.states
18942
+ };
18943
+ }
18944
+
18945
+ // ../core/src/product-review.ts
18946
+ function has2(set, value) {
18947
+ return set.has(value);
18948
+ }
18949
+ function addIssue(issues, issue) {
18950
+ issues.push(issue);
18951
+ }
18952
+ function reviewProductPlan(plan) {
18953
+ const issues = [];
18954
+ const roleIds = new Set(plan.roles.map((role) => role.id));
18955
+ const workflowIds = new Set(plan.workflows.map((workflow) => workflow.id));
18956
+ const resourceIds = new Set(plan.resources.map((resource) => resource.id));
18957
+ const surfaceIds = new Set(plan.surfaces.map((surface) => surface.id));
18958
+ const actionIds = new Set(plan.actions.map((action) => action.id));
18959
+ const runtimeNames = new Map;
18960
+ for (const resource of plan.resources) {
18961
+ const runtimeName = resource.runtimeName ?? resource.name;
18962
+ const previous = runtimeNames.get(runtimeName);
18963
+ if (previous !== undefined) {
18964
+ addIssue(issues, {
18965
+ code: "resource.duplicate-runtime-name",
18966
+ severity: "error",
18967
+ message: `Resources "${resource.label}" and "${previous}" resolve to the same runtime name "${runtimeName}".`,
18968
+ path: `resources.${resource.id}.runtimeName`,
18969
+ suggestion: "Give every generated resource route a unique runtime name."
18970
+ });
18971
+ } else {
18972
+ runtimeNames.set(runtimeName, resource.label);
18973
+ }
18974
+ }
18975
+ const surfaceRoutes = new Map;
18976
+ for (const surface of plan.surfaces) {
18977
+ const route = surface.route ?? `/workflows/${surface.id}`;
18978
+ const previous = surfaceRoutes.get(route);
18979
+ if (previous !== undefined) {
18980
+ addIssue(issues, {
18981
+ code: "surface.duplicate-route",
18982
+ severity: "error",
18983
+ message: `Surfaces "${surface.name}" and "${previous}" resolve to the same route "${route}".`,
18984
+ path: `surfaces.${surface.id}.route`,
18985
+ suggestion: "Give every generated surface a unique route."
18986
+ });
18987
+ } else {
18988
+ surfaceRoutes.set(route, surface.name);
18989
+ }
18990
+ }
18991
+ for (const workflow of plan.workflows) {
18992
+ if (!workflow.actorRoleIds.length)
18993
+ addIssue(issues, { code: "workflow.missing-actor", severity: "error", message: `Workflow "${workflow.name}" has no actor role.`, path: `workflows.${workflow.id}.actorRoleIds` });
18994
+ for (const roleId of workflow.actorRoleIds)
18995
+ if (!has2(roleIds, roleId))
18996
+ addIssue(issues, { code: "workflow.unknown-role", severity: "error", message: `Workflow "${workflow.name}" references unknown role "${roleId}".`, path: `workflows.${workflow.id}.actorRoleIds` });
18997
+ if (!workflow.goal.trim())
18998
+ addIssue(issues, { code: "workflow.missing-goal", severity: "error", message: `Workflow "${workflow.name}" has no user goal.`, path: `workflows.${workflow.id}.goal` });
18999
+ }
19000
+ for (const resource of plan.resources) {
19001
+ if (!resource.workflowIds.length && !resource.internal)
19002
+ addIssue(issues, { code: "resource.no-workflow", severity: "warning", message: `Resource "${resource.label}" is not attached to a workflow.`, path: `resources.${resource.id}.workflowIds`, suggestion: "Make it contextual/internal or attach it to a real task." });
19003
+ for (const workflowId of resource.workflowIds)
19004
+ if (!has2(workflowIds, workflowId))
19005
+ addIssue(issues, { code: "resource.unknown-workflow", severity: "error", message: `Resource "${resource.label}" references unknown workflow "${workflowId}".`, path: `resources.${resource.id}.workflowIds` });
19006
+ for (const roleId of resource.roleIds)
19007
+ if (!has2(roleIds, roleId))
19008
+ addIssue(issues, { code: "resource.unknown-role", severity: "error", message: `Resource "${resource.label}" references unknown role "${roleId}".`, path: `resources.${resource.id}.roleIds` });
19009
+ resource.fields.forEach((field) => {
19010
+ for (const roleId of field.readableBy)
19011
+ if (!has2(roleIds, roleId))
19012
+ addIssue(issues, { code: "field.unknown-role", severity: "error", message: `Field "${field.label}" references unknown readable role "${roleId}".`, path: `resources.${resource.id}.fields.${field.id}.readableBy` });
19013
+ for (const roleId of field.writableBy)
19014
+ if (!has2(roleIds, roleId))
19015
+ addIssue(issues, { code: "field.unknown-role", severity: "error", message: `Field "${field.label}" references unknown writable role "${roleId}".`, path: `resources.${resource.id}.fields.${field.id}.writableBy` });
19016
+ for (const workflowId of field.usedByWorkflowIds)
19017
+ if (!has2(workflowIds, workflowId))
19018
+ addIssue(issues, { code: "field.unknown-workflow", severity: "error", message: `Field "${field.label}" references unknown workflow "${workflowId}".`, path: `resources.${resource.id}.fields.${field.id}.usedByWorkflowIds` });
19019
+ });
19020
+ }
19021
+ for (const surface of plan.surfaces) {
19022
+ reviewSurface(surface, { workflowIds, actionIds, resourceIds, roleIds, surfaceIds }, issues);
19023
+ }
19024
+ const visitingSurfaces = new Set;
19025
+ const visitedSurfaces = new Set;
19026
+ const visitSurface = (id) => {
19027
+ if (visitingSurfaces.has(id)) {
19028
+ addIssue(issues, { code: "surface.cycle", severity: "error", message: `Surface hierarchy contains a cycle at "${id}".`, path: `surfaces.${id}.parentSurfaceId` });
19029
+ return;
19030
+ }
19031
+ if (visitedSurfaces.has(id))
19032
+ return;
19033
+ visitingSurfaces.add(id);
19034
+ const parentId = plan.surfaces.find((surface) => surface.id === id)?.parentSurfaceId;
19035
+ if (parentId !== undefined && surfaceIds.has(parentId))
19036
+ visitSurface(parentId);
19037
+ visitingSurfaces.delete(id);
19038
+ visitedSurfaces.add(id);
19039
+ };
19040
+ for (const surface of plan.surfaces)
19041
+ visitSurface(surface.id);
19042
+ const menuById = new Map(plan.menu.map((item) => [item.id, item]));
19043
+ for (const item of plan.menu) {
19044
+ if (item.parentId !== undefined && !menuById.has(item.parentId)) {
19045
+ addIssue(issues, { code: "menu.unknown-parent", severity: "error", message: `Menu item "${item.label}" references unknown parent "${item.parentId}".`, path: `menu.${item.id}.parentId` });
19046
+ }
19047
+ }
19048
+ const visiting = new Set;
19049
+ const visited = new Set;
19050
+ const visitMenu = (id) => {
19051
+ if (visiting.has(id)) {
19052
+ addIssue(issues, { code: "menu.cycle", severity: "error", message: `Menu hierarchy contains a cycle at "${id}".`, path: `menu.${id}.parentId` });
19053
+ return;
19054
+ }
19055
+ if (visited.has(id))
19056
+ return;
19057
+ visiting.add(id);
19058
+ const parentId = menuById.get(id)?.parentId;
19059
+ if (parentId !== undefined && menuById.has(parentId))
19060
+ visitMenu(parentId);
19061
+ visiting.delete(id);
19062
+ visited.add(id);
19063
+ };
19064
+ for (const item of plan.menu)
19065
+ visitMenu(item.id);
19066
+ for (const item of plan.menu) {
19067
+ if (!item.workflowIds.length && !item.resourceId && !item.surfaceId)
19068
+ addIssue(issues, { code: "menu.no-business-target", severity: "warning", message: `Menu item "${item.label}" has no workflow, resource or surface target.`, path: `menu.${item.id}`, suggestion: "Keep it only for an intentional external/system entry." });
19069
+ if (item.surfaceId && !has2(surfaceIds, item.surfaceId))
19070
+ addIssue(issues, { code: "menu.unknown-surface", severity: "error", message: `Menu item "${item.label}" references unknown surface.`, path: `menu.${item.id}.surfaceId` });
19071
+ if (item.resourceId && !has2(resourceIds, item.resourceId))
19072
+ addIssue(issues, { code: "menu.unknown-resource", severity: "error", message: `Menu item "${item.label}" references unknown resource.`, path: `menu.${item.id}.resourceId` });
19073
+ for (const workflowId of item.workflowIds)
19074
+ if (!has2(workflowIds, workflowId))
19075
+ addIssue(issues, { code: "menu.unknown-workflow", severity: "error", message: `Menu item "${item.label}" references unknown workflow "${workflowId}".`, path: `menu.${item.id}.workflowIds` });
19076
+ for (const roleId of item.roleIds)
19077
+ if (!has2(roleIds, roleId))
19078
+ addIssue(issues, { code: "menu.unknown-role", severity: "error", message: `Menu item "${item.label}" references unknown role "${roleId}".`, path: `menu.${item.id}.roleIds` });
19079
+ if (!item.roleIds.length && item.placement !== "hidden")
19080
+ addIssue(issues, { code: "menu.public-entry", severity: "info", message: `Menu item "${item.label}" is public at the UI hint layer.`, path: `menu.${item.id}.roleIds`, suggestion: "Keep server authorization independent from menu visibility." });
19081
+ }
19082
+ for (const action of plan.actions) {
19083
+ if (!has2(workflowIds, action.workflowId))
19084
+ addIssue(issues, { code: "action.unknown-workflow", severity: "error", message: `Action "${action.label}" references unknown workflow.`, path: `actions.${action.id}.workflowId` });
19085
+ for (const roleId of action.roleIds)
19086
+ if (!has2(roleIds, roleId))
19087
+ addIssue(issues, { code: "action.unknown-role", severity: "error", message: `Action "${action.label}" references unknown role "${roleId}".`, path: `actions.${action.id}.roleIds` });
19088
+ if (!action.roleIds.length)
19089
+ addIssue(issues, { code: "action.missing-roles", severity: "warning", message: `Action "${action.label}" has no role visibility contract.`, path: `actions.${action.id}.roleIds` });
19090
+ }
19091
+ const served = new Set;
19092
+ plan.surfaces.forEach((surface) => surface.workflowIds.forEach((id) => served.add(id)));
19093
+ plan.menu.forEach((item) => item.workflowIds.forEach((id) => served.add(id)));
19094
+ const unserved = plan.workflows.filter((workflow) => !served.has(workflow.id));
19095
+ unserved.forEach((workflow) => addIssue(issues, { code: "workflow.unserved", severity: workflow.priority === "core" ? "error" : "warning", message: `Workflow "${workflow.name}" has no page or menu entry.`, path: `workflows.${workflow.id}`, suggestion: "Add a surface or contextual entry, or mark the workflow as deferred." }));
19096
+ const roleVisibilityGaps = plan.workflows.filter((workflow) => workflow.actorRoleIds.length > 0 && !plan.surfaces.some((surface) => surface.workflowIds.includes(workflow.id) && (surface.roleIds.length === 0 || workflow.actorRoleIds.some((roleId) => surface.roleIds.includes(roleId))))).length;
19097
+ const orphanResourceCount = plan.resources.filter((resource) => resource.workflowIds.length === 0 && !resource.internal).length;
19098
+ const errors = issues.filter((issue) => issue.severity === "error");
19099
+ return {
19100
+ ok: errors.length === 0,
19101
+ issues,
19102
+ metrics: {
19103
+ resourceCount: plan.resources.length,
19104
+ workflowCount: plan.workflows.length,
19105
+ surfaceCount: plan.surfaces.length,
19106
+ menuNodeCount: plan.menu.length,
19107
+ orphanResourceCount,
19108
+ unservedWorkflowCount: unserved.length,
19109
+ roleVisibilityGaps
19110
+ }
19111
+ };
19112
+ }
19113
+ function reviewSurface(surface, refs, issues) {
19114
+ if (!surface.purpose.trim())
19115
+ addIssue(issues, { code: "surface.missing-purpose", severity: "error", message: `Surface "${surface.name}" has no purpose.`, path: `surfaces.${surface.id}.purpose` });
19116
+ for (const roleId of surface.roleIds)
19117
+ if (!refs.roleIds.has(roleId))
19118
+ addIssue(issues, { code: "surface.unknown-role", severity: "error", message: `Surface "${surface.name}" references unknown role "${roleId}".`, path: `surfaces.${surface.id}.roleIds` });
19119
+ if (surface.parentSurfaceId !== undefined && !refs.surfaceIds.has(surface.parentSurfaceId))
19120
+ addIssue(issues, { code: "surface.unknown-parent", severity: "error", message: `Surface "${surface.name}" references unknown parent surface "${surface.parentSurfaceId}".`, path: `surfaces.${surface.id}.parentSurfaceId` });
19121
+ if (!surface.workflowIds.length)
19122
+ addIssue(issues, { code: "surface.no-workflow", severity: "warning", message: `Surface "${surface.name}" is not attached to a workflow.`, path: `surfaces.${surface.id}.workflowIds` });
19123
+ surface.workflowIds.forEach((id) => {
19124
+ if (!refs.workflowIds.has(id))
19125
+ addIssue(issues, { code: "surface.unknown-workflow", severity: "error", message: `Surface "${surface.name}" references unknown workflow "${id}".`, path: `surfaces.${surface.id}.workflowIds` });
19126
+ });
19127
+ surface.resourceIds.forEach((id) => {
19128
+ if (!refs.resourceIds.has(id))
19129
+ addIssue(issues, { code: "surface.unknown-resource", severity: "error", message: `Surface "${surface.name}" references unknown resource "${id}".`, path: `surfaces.${surface.id}.resourceIds` });
19130
+ });
19131
+ surface.actionIds.forEach((id) => {
19132
+ if (!refs.actionIds.has(id))
19133
+ addIssue(issues, { code: "surface.unknown-action", severity: "error", message: `Surface "${surface.name}" references unknown action "${id}".`, path: `surfaces.${surface.id}.actionIds` });
19134
+ });
19135
+ if (surface.primaryActionId && !refs.actionIds.has(surface.primaryActionId))
19136
+ addIssue(issues, { code: "surface.unknown-primary-action", severity: "error", message: `Surface "${surface.name}" references unknown primary action.`, path: `surfaces.${surface.id}.primaryActionId` });
19137
+ if (!surface.states.length)
19138
+ addIssue(issues, { code: "surface.missing-states", severity: "warning", message: `Surface "${surface.name}" does not declare empty/loading/error/permission states.`, path: `surfaces.${surface.id}.states`, suggestion: "Declare the states the Svelte view must implement." });
19139
+ if (surface.sections.length > 6)
19140
+ addIssue(issues, { code: "surface.section-density", severity: "warning", message: `Surface "${surface.name}" contains ${surface.sections.length} sections.`, path: `surfaces.${surface.id}.sections`, suggestion: "Group related information or use progressive disclosure." });
19141
+ if (surface.actionIds.length > 4)
19142
+ addIssue(issues, { code: "surface.action-density", severity: "warning", message: `Surface "${surface.name}" exposes ${surface.actionIds.length} actions.`, path: `surfaces.${surface.id}.actionIds`, suggestion: "Keep one primary action and move secondary actions into context or More." });
19143
+ }
19144
+
19145
+ // ../core/src/product-pipeline.ts
19146
+ function runProductPipeline(input) {
19147
+ const navigation = buildProductNavigation(input.plan, input.navigation);
19148
+ const visibleSurfaceIds = input.surfaceIds === undefined ? undefined : new Set(input.surfaceIds);
19149
+ const pages = input.plan.surfaces.filter((surface) => visibleSurfaceIds === undefined || visibleSurfaceIds.has(surface.id)).filter((surface) => surface.roleIds.length === 0 || surface.roleIds.some((roleId) => input.navigation.roleIds.includes(roleId))).map((surface) => buildProductPagePlan(surface, input.plan));
19150
+ return { navigation, pages, productReview: reviewProductPlan(input.plan) };
19151
+ }
19152
+ function formatProductPreview(output) {
19153
+ const lines = ["Navigation:"];
19154
+ for (const node of output.navigation) {
19155
+ if (!node.visible)
19156
+ continue;
19157
+ lines.push(`- ${node.label}`);
19158
+ for (const child of node.children)
19159
+ if (child.visible)
19160
+ lines.push(` - ${child.label}`);
19161
+ }
19162
+ lines.push("", "Pages:");
19163
+ output.pages.forEach((page) => lines.push(`- ${page.title}: ${page.purpose}`));
19164
+ lines.push("", `Product review: ${output.productReview.ok ? "pass" : "fail"}`);
19165
+ return lines.join(`
19166
+ `);
19167
+ }
19168
+
19169
+ // src/vibe-command.ts
18688
19170
  var PRESETS = ["operations", "enterprise", "collaboration"];
18689
19171
  function parseVibeArguments(args) {
18690
19172
  const [command, ...rest] = args;
@@ -18692,6 +19174,31 @@ function parseVibeArguments(args) {
18692
19174
  return { command };
18693
19175
  if (command === "catalog" && rest.length === 0)
18694
19176
  return { command };
19177
+ if (command === "plan" || command === "review") {
19178
+ const positional = [];
19179
+ const roleIds = [];
19180
+ for (let i = 0;i < rest.length; i++) {
19181
+ const arg = rest[i];
19182
+ if (arg === undefined)
19183
+ continue;
19184
+ if (arg === "--role") {
19185
+ const roleId = rest[++i];
19186
+ if (!roleId?.trim())
19187
+ throw new Error(`Missing value for --role`);
19188
+ roleIds.push(roleId);
19189
+ } else if (arg.startsWith("-")) {
19190
+ throw new Error(`Unknown option: ${arg}`);
19191
+ } else {
19192
+ positional.push(arg);
19193
+ }
19194
+ }
19195
+ if (positional.length !== 1)
19196
+ throw new Error(`vibe ${command} requires exactly one product plan JSON file`);
19197
+ const planFile = positional[0];
19198
+ if (planFile === undefined)
19199
+ throw new Error("Missing product plan JSON file");
19200
+ return { command, planFile: path6.resolve(planFile), roleIds };
19201
+ }
18695
19202
  if (command === "catalog" && rest.length === 2 && rest[0] === "--query" && rest[1]?.trim()) {
18696
19203
  return { command, query: rest[1] };
18697
19204
  }
@@ -18699,9 +19206,11 @@ function parseVibeArguments(args) {
18699
19206
  return { command, page: rest[0] };
18700
19207
  }
18701
19208
  if (command !== "init")
18702
- throw new Error("Usage: vibe mcp | vibe catalog [--query <keywords>] | vibe inspect <page> | vibe init <directory> [--preset operations|enterprise|collaboration] [--write]");
19209
+ throw new Error("Usage: vibe mcp | vibe catalog [--query <keywords>] | vibe inspect <page> | vibe plan <product-plan.json> [--role <role-id>] | vibe review <product-plan.json> | vibe init <directory> [--preset operations|enterprise|collaboration] [--product-plan <file>] [--role <role-id>] [--write]");
18703
19210
  let preset = "enterprise";
18704
19211
  let write = false;
19212
+ let productPlanFile;
19213
+ const roleIds = [];
18705
19214
  const positional = [];
18706
19215
  for (let i = 0;i < rest.length; i++) {
18707
19216
  const arg = rest[i];
@@ -18714,6 +19223,16 @@ function parseVibeArguments(args) {
18714
19223
  if (!PRESETS.some((p) => p === value))
18715
19224
  throw new Error(`Unknown design preset: ${value ?? "(missing)"}`);
18716
19225
  preset = value;
19226
+ } else if (arg === "--product-plan" || arg === "--plan") {
19227
+ const value = rest[++i];
19228
+ if (!value?.trim())
19229
+ throw new Error(`Missing value for ${arg}`);
19230
+ productPlanFile = path6.resolve(value);
19231
+ } else if (arg === "--role") {
19232
+ const value = rest[++i];
19233
+ if (!value?.trim())
19234
+ throw new Error("Missing value for --role");
19235
+ roleIds.push(value);
18717
19236
  } else if (arg.startsWith("-"))
18718
19237
  throw new Error(`Unknown option: ${arg}`);
18719
19238
  else
@@ -18724,7 +19243,7 @@ function parseVibeArguments(args) {
18724
19243
  const directory = positional[0];
18725
19244
  if (directory === undefined)
18726
19245
  throw new Error("Missing project directory");
18727
- return { command, directory: path6.resolve(directory), preset, write };
19246
+ return { command, directory: path6.resolve(directory), preset, write, productPlanFile, roleIds };
18728
19247
  }
18729
19248
  function readTree(directory, prefix = "") {
18730
19249
  const files = {};
@@ -18740,8 +19259,119 @@ function readTree(directory, prefix = "") {
18740
19259
  }
18741
19260
  return files;
18742
19261
  }
18743
- function planVibeProject(packageRoot, manifest, preset) {
18744
- const blueprint = path6.join(packageRoot, "blueprints/customer-workspace");
19262
+ function productRoute(surface) {
19263
+ return surface.route ?? "/workflows/" + surface.id;
19264
+ }
19265
+ function buildProductGeneratedFiles(plan, selectedRoleIds) {
19266
+ const allRoleIds = plan.roles.map((role) => role.id);
19267
+ const roleIds = selectedRoleIds.length ? selectedRoleIds : allRoleIds;
19268
+ const planJson = JSON.stringify(plan, null, 2);
19269
+ const summary = [
19270
+ "# " + plan.brief.name,
19271
+ "",
19272
+ plan.brief.description,
19273
+ "",
19274
+ `## Primary outcome
19275
+ ` + plan.brief.primaryOutcome,
19276
+ `## Audience
19277
+ ` + plan.brief.primaryAudience,
19278
+ `## Roles
19279
+ ` + (plan.roles.map((role) => "- **" + role.name + "** (" + role.id + "):" + (role.description ?? "未提供描述")).join(`
19280
+ `) || "- 未定义角色"),
19281
+ `## Workflows
19282
+ ` + (plan.workflows.map((workflow) => "- **" + workflow.name + "**:" + workflow.goal + "(" + workflow.priority + " / " + workflow.frequency + ")").join(`
19283
+ `) || "- 未定义工作流"),
19284
+ `## Surfaces
19285
+ ` + (plan.surfaces.map((surface) => "- **" + surface.name + "**:" + productRoute(surface) + "," + surface.purpose).join(`
19286
+ `) || "- 未定义页面"),
19287
+ `## Menu
19288
+ ` + (plan.menu.map((item) => "- " + item.label + "(" + item.placement + ")").join(`
19289
+ `) || "- 未定义菜单入口"),
19290
+ "## Generation notes",
19291
+ "- Generated resources are read-only metadata until an explicit contract is added.",
19292
+ "- Product actions are metadata and do not imply backend mutations.",
19293
+ "- Initial visible roles: " + (roleIds.join(", ") || "all") + "."
19294
+ ].join(`
19295
+
19296
+ `) + `
19297
+ `;
19298
+ const resourcesSource = [
19299
+ "import type { FieldDefinition, ResourceDefinition } from '@svadmin/core';",
19300
+ "import { productPlan } from './product-plan';",
19301
+ "",
19302
+ "const supportedFieldTypes = new Set<string>(['text','number','boolean','date','time','datetime','daterange','select','multiselect','tags','textarea','richtext','image','images','json','relation','color','url','email','phone','currency','percent','file','markdown','password','array','tree-select','treeselect','cascader','transfer','rate','rating','avatar','copy','code']);",
19303
+ "function toFieldType(type: string): FieldDefinition['type'] {",
19304
+ " return (supportedFieldTypes.has(type) ? type : 'text') as FieldDefinition['type'];",
19305
+ "}",
19306
+ "function toField(field: (typeof productPlan.resources)[number]['fields'][number]): FieldDefinition {",
19307
+ " const hidden = field.sensitive === true || field.disclosure === 'hidden';",
19308
+ " return { key: field.id, label: field.label, type: toFieldType(field.type), required: field.required, showInList: !hidden, showInShow: !hidden, showInForm: !hidden, showInCreate: !hidden, showInEdit: !hidden };",
19309
+ "}",
19310
+ "export const productResources: ResourceDefinition[] = productPlan.resources.map(resource => ({",
19311
+ " name: resource.runtimeName ?? resource.name, label: resource.label,",
19312
+ " icon: resource.menuPlacement === 'settings' ? 'settings' : undefined,",
19313
+ " fields: resource.fields.map(toField), canCreate: false, canEdit: false, canDelete: false, canShow: true,",
19314
+ " showInMenu: resource.internal !== true && resource.menuPlacement !== 'hidden' && resource.menuPlacement !== 'context' && resource.menuPlacement !== 'command',",
19315
+ "}));",
19316
+ ""
19317
+ ].join(`
19318
+ `);
19319
+ const generatedPlan = [
19320
+ "import { parseProductPlan, type ProductPlan } from '@svadmin/core/product-plan';",
19321
+ "",
19322
+ "export const productPlan: ProductPlan = parseProductPlan(" + planJson + ");",
19323
+ ""
19324
+ ].join(`
19325
+ `);
19326
+ const generatedNavigation = [
19327
+ "import type { MenuItem } from '@svadmin/core';",
19328
+ "import { buildProductMenuItems } from '@svadmin/core/product-navigation';",
19329
+ "import { productPlan } from './product-plan';",
19330
+ "",
19331
+ "export const ALL_PRODUCT_ROLE_IDS = " + JSON.stringify(allRoleIds) + " as const;",
19332
+ "export const INITIAL_PRODUCT_ROLE_IDS = " + JSON.stringify(roleIds) + " as const;",
19333
+ "export function createProductMenu(roleIds: readonly string[] = INITIAL_PRODUCT_ROLE_IDS, featureFlags: Readonly<Record<string, boolean>> = {}): MenuItem[] {",
19334
+ " return buildProductMenuItems(productPlan, { roleIds, featureFlags });",
19335
+ "}",
19336
+ "export const productMenu = createProductMenu();",
19337
+ ""
19338
+ ].join(`
19339
+ `);
19340
+ const generatedPages = [
19341
+ "import { runProductPipeline } from '@svadmin/core/product-pipeline';",
19342
+ "import { productPlan } from './product-plan';",
19343
+ "import { INITIAL_PRODUCT_ROLE_IDS } from './navigation';",
19344
+ "",
19345
+ "export const productPipeline = runProductPipeline({ plan: productPlan, navigation: { roleIds: [...INITIAL_PRODUCT_ROLE_IDS] } });",
19346
+ "export const productPagePlans = productPipeline.pages;",
19347
+ "export const productSurfaceRoutes = Object.fromEntries(productPlan.surfaces.map(surface => [surface.route ?? '/workflows/' + surface.id, surface.id])) as Record<string, string>;",
19348
+ ""
19349
+ ].join(`
19350
+ `);
19351
+ const generatedRoutes = [
19352
+ "import type { AdminPageRoute } from '@svadmin/ui';",
19353
+ "import ProductSurfacePage from '../ProductSurfacePage.svelte';",
19354
+ "import { productSurfaceRoutes } from './pages';",
19355
+ "",
19356
+ "export const productPageRoutes: readonly AdminPageRoute[] = Object.keys(productSurfaceRoutes).map(path => ({ path, component: ProductSurfacePage }));",
19357
+ ""
19358
+ ].join(`
19359
+ `);
19360
+ return {
19361
+ "svadmin.product.json": planJson + `
19362
+ `,
19363
+ "PRODUCT.md": summary,
19364
+ "src/generated/product-plan.ts": generatedPlan,
19365
+ "src/generated/resources.ts": resourcesSource,
19366
+ "src/generated/navigation.ts": generatedNavigation,
19367
+ "src/generated/pages.ts": generatedPages,
19368
+ "src/generated/page-routes.ts": generatedRoutes,
19369
+ "src/generated/README.md": `Generated product files. Derived from svadmin.product.json; regenerate instead of editing by hand.
19370
+ `
19371
+ };
19372
+ }
19373
+ function planVibeProject(packageRoot, manifest, preset, options = {}) {
19374
+ const blueprint = path6.join(packageRoot, options.productPlan ? "blueprints/product-workspace" : "blueprints/customer-workspace");
18745
19375
  const files = readTree(blueprint);
18746
19376
  const template = path6.join(packageRoot, "template");
18747
19377
  for (const name of ["index.html", "svelte.config.js", "tsconfig.json", "vite.config.ts", "src/main.ts", "src/vite-env.d.ts"]) {
@@ -18752,15 +19382,19 @@ function planVibeProject(packageRoot, manifest, preset) {
18752
19382
  }
18753
19383
  files["DESIGN.md"] = fs6.readFileSync(path6.join(packageRoot, "guidance/DESIGN.md"), "utf8");
18754
19384
  files["AGENTS.md"] = fs6.readFileSync(path6.join(packageRoot, "guidance/AGENTS.md"), "utf8");
19385
+ files["PRODUCT.md"] = fs6.readFileSync(path6.join(packageRoot, "guidance/PRODUCT.md"), "utf8");
19386
+ if (options.productPlan)
19387
+ Object.assign(files, buildProductGeneratedFiles(options.productPlan, options.roleIds ?? []));
18755
19388
  const skill = files["vibe-skill.md"];
18756
19389
  if (typeof skill !== "string")
18757
19390
  throw new Error("The shipped vibe skill is missing");
18758
19391
  files[".agents/skills/svadmin-vibe/SKILL.md"] = skill;
18759
19392
  delete files["vibe-skill.md"];
18760
- files["src/design-selection.ts"] = `export const initialPreset = '${preset}' as const;
19393
+ if (!options.productPlan)
19394
+ files["src/design-selection.ts"] = `export const initialPreset = '${preset}' as const;
18761
19395
  `;
18762
19396
  const pkg = createProjectPackageJson(manifest, {
18763
- projectName: "svadmin-customer-workspace",
19397
+ projectName: options.productPlan ? "svadmin-product-workspace" : "svadmin-customer-workspace",
18764
19398
  dataProvider: "none",
18765
19399
  authProvider: "none"
18766
19400
  });
@@ -18777,31 +19411,36 @@ function planVibeProject(packageRoot, manifest, preset) {
18777
19411
  authProvider: "none",
18778
19412
  ...manifest.dependencies["@svadmin/core"] === undefined ? {} : { coreVersionRange: manifest.dependencies["@svadmin/core"] }
18779
19413
  });
18780
- ai.resources = resources.map((resource) => ({
18781
- name: resource.name,
18782
- label: resource.label ?? resource.name,
18783
- fields: (resource.fields ?? []).map((field) => ({
18784
- key: field.key,
18785
- label: field.label,
18786
- type: field.type,
18787
- required: field.required ?? false
18788
- })),
18789
- operations: uiOnlyResources.includes(resource.name) ? [] : [
18790
- "list",
18791
- ...resource.canShow === false ? [] : ["show"],
18792
- ...resource.canCreate === false ? [] : ["create"],
18793
- ...resource.canEdit === false ? [] : ["edit"]
18794
- ]
18795
- }));
18796
- ai.routes = ai.resources.map((resource) => ({
18797
- resource: resource.name,
18798
- path: `/${resource.name}`,
18799
- operations: uiOnlyResources.includes(resource.name) ? ["list"] : resource.operations
18800
- }));
19414
+ if (options.productPlan) {
19415
+ ai.resources = options.productPlan.resources.map((resource) => ({ name: resource.runtimeName ?? resource.name, label: resource.label, fields: resource.fields.map((field) => ({ key: field.id, label: field.label, type: field.type, required: field.required ?? false })), operations: ["list", "show"] }));
19416
+ ai.routes = options.productPlan.surfaces.map((surface) => ({ resource: surface.resourceIds[0] ?? "", path: productRoute(surface), operations: ["list", "show"] }));
19417
+ } else {
19418
+ ai.resources = resources.map((resource) => ({
19419
+ name: resource.name,
19420
+ label: resource.label ?? resource.name,
19421
+ fields: (resource.fields ?? []).map((field) => ({
19422
+ key: field.key,
19423
+ label: field.label,
19424
+ type: field.type,
19425
+ required: field.required ?? false
19426
+ })),
19427
+ operations: uiOnlyResources.includes(resource.name) ? [] : [
19428
+ "list",
19429
+ ...resource.canShow === false ? [] : ["show"],
19430
+ ...resource.canCreate === false ? [] : ["create"],
19431
+ ...resource.canEdit === false ? [] : ["edit"]
19432
+ ]
19433
+ }));
19434
+ ai.routes = ai.resources.map((resource) => ({
19435
+ resource: resource.name,
19436
+ path: `/${resource.name}`,
19437
+ operations: uiOnlyResources.includes(resource.name) ? ["list"] : resource.operations
19438
+ }));
19439
+ }
18801
19440
  ai.providers.data = { choice: "none", package: null, capabilities: ["in-memory-demo", "data", "filter", "sort", "pagination"] };
18802
19441
  ai.project.commands = pkg.scripts;
18803
19442
  ai.project.testCommand = "bun run test:ui";
18804
- ai.guidance = [...ai.guidance, "svadmin.vibe.json", "ARCHITECTURE.md", ".agents/skills/svadmin-vibe/SKILL.md"];
19443
+ ai.guidance = [...ai.guidance, "PRODUCT.md", "svadmin.vibe.json", "ARCHITECTURE.md", ".agents/skills/svadmin-vibe/SKILL.md"];
18805
19444
  files["svadmin.ai.json"] = `${JSON.stringify(ai, null, 2)}
18806
19445
  `;
18807
19446
  files["svadmin.schema.json"] = `${JSON.stringify(buildAdminSchemaJson(), null, 2)}
@@ -18850,10 +19489,35 @@ async function vibeCommand(args, packageRoot, manifest) {
18850
19489
  console.log(JSON.stringify(inspectVibePage(packageRoot, options.page), null, 2));
18851
19490
  return;
18852
19491
  }
19492
+ if (options.command === "review" || options.command === "plan") {
19493
+ const input = JSON.parse(fs6.readFileSync(options.planFile, "utf8"));
19494
+ const plan = parseProductPlan(input);
19495
+ if (options.command === "review") {
19496
+ console.log(JSON.stringify(reviewProductPlan(plan), null, 2));
19497
+ return;
19498
+ }
19499
+ const roleIds = options.roleIds.length ? options.roleIds : plan.roles.map((role) => role.id);
19500
+ console.log(formatProductPreview(runProductPipeline({ plan, navigation: { roleIds } })));
19501
+ return;
19502
+ }
19503
+ if (options.command !== "init")
19504
+ throw new Error("Unsupported vibe command");
18853
19505
  if (fs6.existsSync(options.directory))
18854
19506
  throw new Error("The target already exists. Choose a new directory; existing projects are never overwritten.");
18855
- const files = planVibeProject(packageRoot, manifest, options.preset);
18856
- console.log(`svadmin vibe: customer-workspace / ${options.preset}
19507
+ let productPlan;
19508
+ if (options.productPlanFile) {
19509
+ const input = JSON.parse(fs6.readFileSync(options.productPlanFile, "utf8"));
19510
+ productPlan = parseProductPlan(input);
19511
+ const report = reviewProductPlan(productPlan);
19512
+ if (!report.ok)
19513
+ throw new Error(`Product plan review failed:
19514
+ ` + JSON.stringify(report, null, 2));
19515
+ if (report.issues.length)
19516
+ console.warn("Product plan review warnings: " + report.issues.length);
19517
+ }
19518
+ const productOptions = { roleIds: options.roleIds, ...productPlan === undefined ? {} : { productPlan } };
19519
+ const files = planVibeProject(packageRoot, manifest, options.preset, productOptions);
19520
+ console.log(`svadmin vibe: ${productPlan ? "product-workspace" : "customer-workspace"} / ${options.preset}
18857
19521
  ${options.directory}`);
18858
19522
  for (const file of Object.keys(files).sort())
18859
19523
  console.log(` add ${file}`);
@@ -18991,7 +19655,7 @@ function upgrade(args) {
18991
19655
  const upgradeExecution = commandArguments.write ? writeProjectPackageJsonUpgrade(packagePath, scaffoldManifest, new Date) : planProjectPackageFileUpgrade(packagePath, scaffoldManifest);
18992
19656
  printUpgradeExecution(upgradeExecution, commandArguments.projectDirectory, packagePath);
18993
19657
  }
18994
- var GUIDANCE_FILES = ["DESIGN.md", "AGENTS.md"];
19658
+ var GUIDANCE_FILES = ["DESIGN.md", "AGENTS.md", "PRODUCT.md"];
18995
19659
  function missingGuidanceFiles(projectDirectory) {
18996
19660
  return GUIDANCE_FILES.filter((fileName) => !fs7.existsSync(path7.join(projectDirectory, fileName)));
18997
19661
  }