openxiangda 1.0.214 → 1.0.216

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.
Files changed (40) hide show
  1. package/README.md +5 -1
  2. package/lib/application-environments.js +227 -3
  3. package/lib/cli.js +1521 -136
  4. package/lib/policy.js +1 -0
  5. package/lib/release-mainline.js +67 -8
  6. package/lib/resource-binding-contract.js +210 -0
  7. package/lib/sdd.js +216 -74
  8. package/lib/workspace-init.js +9 -3
  9. package/lib/worktree-cleanup.js +631 -0
  10. package/lib/worktree-owner.js +121 -5
  11. package/openxiangda-skills/SKILL.md +11 -4
  12. package/openxiangda-skills/references/openxiangda-api.md +1 -1
  13. package/openxiangda-skills/references/workspace-state.md +1 -1
  14. package/openxiangda-skills/skills/openxiangda-core/SKILL.md +9 -1
  15. package/package.json +8 -2
  16. package/packages/sdk/dist/runtime/index.cjs +3305 -1752
  17. package/packages/sdk/dist/runtime/index.cjs.map +1 -1
  18. package/packages/sdk/dist/runtime/index.d.mts +1 -1
  19. package/packages/sdk/dist/runtime/index.d.ts +1 -1
  20. package/packages/sdk/dist/runtime/index.mjs +3275 -1699
  21. package/packages/sdk/dist/runtime/index.mjs.map +1 -1
  22. package/packages/sdk/dist/runtime/react.cjs +1572 -19
  23. package/packages/sdk/dist/runtime/react.cjs.map +1 -1
  24. package/packages/sdk/dist/runtime/react.d.mts +219 -2
  25. package/packages/sdk/dist/runtime/react.d.ts +219 -2
  26. package/packages/sdk/dist/runtime/react.mjs +1595 -19
  27. package/packages/sdk/dist/runtime/react.mjs.map +1 -1
  28. package/packages/sdk/source/admin-list/AdminList.tsx +1525 -0
  29. package/packages/sdk/source/admin-list/README.md +12 -0
  30. package/packages/sdk/source/admin-list/dataSources.ts +220 -0
  31. package/packages/sdk/source/admin-list/index.ts +41 -0
  32. package/packages/sdk/source/admin-list/preferences.ts +174 -0
  33. package/packages/sdk/source/admin-list/types.ts +251 -0
  34. package/templates/openxiangda-react-spa/.cursor/rules/openxiangda.mdc +6 -0
  35. package/templates/openxiangda-react-spa/.qoder/rules/openxiangda.md +6 -0
  36. package/templates/openxiangda-react-spa/AGENTS.md +8 -1
  37. package/templates/openxiangda-react-spa/scripts/deploy.mjs +1 -1
  38. package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda.mdc +6 -1
  39. package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda.md +6 -0
  40. package/templates/sy-lowcode-app-workspace/AGENTS.md +9 -1
package/README.md CHANGED
@@ -82,7 +82,7 @@ openxiangda app snapshot APP_XXXX --profile dev --json
82
82
 
83
83
  User tokens are stored in `~/.openxiangda/profiles.json` with `0600` permissions. Shared workspace environment values, including `APP_OSS_*`, can live in `~/.openxiangda/.env` and are inherited by new workspaces. Project `.env` files still work and override the global defaults. Project state is stored in `.openxiangda/state.json` and contains only durable profile-specific resource IDs and environment bindings; volatile candidate/deployment progress lives in the private `.openxiangda/releases/` journal so a release does not dirty the reviewed Git state. Durable CLI writes use a workspace lock plus atomic merge/rename so concurrent processes do not truncate another profile's state.
84
84
 
85
- An environment-managed workspace keeps one logical application with independent `preproduction` and `production` targets. Each target owns its own `appType`, resource IDs, release heads, data, and side-effect policy; IDs must never be copied across targets. `release ship` is deliberately two-phase: the first invocation seals the clean authoritative Git revision and deploys one immutable candidate only to preproduction; it always stops at `awaiting_production_confirmation`. A later invocation includes `--confirm-production` and promotes that exact candidate to production without rebuilding it. Real human acceptance is the recommended default; an optional `--acceptance-note` records it in the deployment evidence, but the platform does not force every low-risk or emergency release through a rigid approval gate. It is impossible to prepare and promote in one invocation. For an audited catch-up whose exact non-delete targets are already merged but whose active resources came from several historical release lineages, the first invocation may explicitly add `--adopt-online-baseline --adoption-reason "..."`; ship validates and freezes this pair before creating a candidate/deployment, then automatically reuses it during the later production confirmation and forwards it only to exact scoped resource stages. A reviewed complete Function/Automation manifest replacement may similarly add the inseparable `--replace-manifest --reason "..."` pair; ship forwards it only to an exact Backend stage and requires the production confirmation to repeat the exact preproduction intent. Frozen online heads, change/lease ownership, server CAS, staged children, and the single App finalize remain mandatory. Supported configuration resources use exact `resourceSelectors`; a legacy `resources=true` category marker is narrowed by those selectors, while unknown, wildcard, destructive, and genuinely unscoped generic resources remain blocked. The lower-level `candidate/deploy/test/promote` commands remain available for recovery and diagnostics. Direct `release publish` is retained only for legacy, unmanaged workspaces and fails closed for either target of an environment-managed application.
85
+ An environment-managed workspace keeps one logical application with independent `preproduction` and `production` targets. Each target owns its own `appType`, resource IDs, release heads, data, and side-effect policy; IDs must never be copied across targets. `release ship` always executes the same ordered candidate preproduction production protocol. The normal first invocation seals the candidate, deploys only to preproduction, and stops at `awaiting_production_confirmation`; a later invocation with `--confirm-production` promotes it. For an explicitly authorized emergency, supplying `--confirm-production` on the first invocation runs both phases in one command without bypassing preproduction, CAS, evidence, or production confirmation. Candidate sealing covers source, `public/`, build controls/scripts, stable environment/resource bindings, and target-specific hashed Runtime artifacts. Both deployments upload those artifacts with `--no-build`; each deployment is completed with evidence and reaches terminal `succeeded`, so it cannot leave the target slot blocked. Unrelated commits may land on authoritative mainline between phases only while the sealed commit remains an ancestor and every sealed input/binding/artifact still validates. Real human acceptance remains the recommended default and `--acceptance-note` records it. For audited historical-lineage adoption or reviewed Backend manifest replacement, the existing paired flags and exact-scope gates remain mandatory. Supported configuration resources use exact `resourceSelectors`; unknown, wildcard, destructive, and genuinely unscoped generic resources remain blocked. Lower-level candidate/deploy/test/promote commands are recovery primitives. Direct `release publish` is retained only for legacy unmanaged workspaces.
86
86
 
87
87
  Exact, non-destructive configuration selectors such as Data Views and permission groups are now sequenced automatically inside the same ship journal instead of requiring separate SDD changes. New forms are idempotently ensured per environment before their immutable FormRelease is staged. Unscoped resources and destructive configuration deletes remain fail-closed.
88
88
 
@@ -108,6 +108,8 @@ Parallel tasks develop and test in isolated worktrees, but feature worktrees do
108
108
 
109
109
  Because promotion starts from an already-pushed authoritative mainline commit, `openxiangda release integration-status --change <change> --profile <name>` should pass immediately after activation. After a managed ship has ended, `--change` recovers lineage directly from the private `ship.json` or follows its production/preproduction deployment IDs to the matching `execution.json`; a recovery failure names the missing file or field. Run it and `release end`; there is no post-release merge step.
110
110
 
111
+ Task worktrees are disposable only after the release lifecycle is closed. From the synchronized canonical main checkout, run `openxiangda workspace cleanup` for a dry-run inventory. A worktree is `SAFE` only when it is OpenXiangda/Codex-managed, its commit is contained in live remote mainline, it is clean and unowned, and it has no private candidate/release journal or promotion lease/baseline. `--apply` is bound to the exact hashed dry-run plan, revalidates under the worktree owner lock, and removes only those reviewed entries plus their unchanged local branches. Newly-safe worktrees are left for the next review. Malformed owner state fails closed. Stale records are reported for explicit manual review because the CLI never runs repository-wide `git worktree prune`. Remote branches are retained.
112
+
111
113
  `openxiangda task status --change <id>` is the compact, read-only answer to
112
114
  “现在到哪一步”:it combines SDD、TaskResult、IntegrationBundle、execution
113
115
  journal、lease 和 post-commit outbox,输出进度、ETA、是否已经发生平台写入、
@@ -175,6 +177,8 @@ L1 quick records are generated in a compact form and should not be expanded into
175
177
 
176
178
  Every concurrent task uses its own Git worktree/branch and development change. The canonical main checkout is reserved for integration and release; tasks never stash/restore each other's files to make it publishable. The mainline release coordinator bundles selected approved changes after merge; dependency impact outside the approved scope is reported as a warning rather than silently widening a small release. Live commands still require canonical exact selectors, and app-wide/delete operations need explicit authority.
177
179
 
180
+ After the mainline release and `release end`, the coordinator runs `workspace cleanup` from canonical main and applies only `SAFE` entries. Dirty/unmerged/owned worktrees and worktrees with private release or promotion state remain blocked for recovery.
181
+
178
182
  Agent investigation is bounded to one complete CodeGraph survey plus at most one focused follow-up, and ordinary work loads one domain skill. Unchanged lease/build waits use `task status --watch` and produce updates only on material transitions.
179
183
 
180
184
  Before confirmation, agents may read, inspect, snapshot, dry-run, ask questions, and output/write the architecture document. They must not edit source files, mutate platform resources, publish, deploy, send notifications, or call live write/delete endpoints.
@@ -570,20 +570,163 @@ function hashWorkspaceFile(cwd, relativePath) {
570
570
  if (
571
571
  !normalized ||
572
572
  relative.startsWith('..') ||
573
- path.isAbsolute(relative) ||
574
- !fs.existsSync(absolute) ||
575
- !fs.statSync(absolute).isFile()
573
+ path.isAbsolute(relative)
576
574
  ) {
577
575
  return null;
578
576
  }
577
+ if (!fs.existsSync(absolute)) {
578
+ return {
579
+ path: normalized,
580
+ exists: false,
581
+ size: 0,
582
+ sha256: null,
583
+ };
584
+ }
585
+ if (!fs.statSync(absolute).isFile()) return null;
579
586
  const buffer = fs.readFileSync(absolute);
580
587
  return {
581
588
  path: normalized,
589
+ exists: true,
582
590
  size: buffer.length,
583
591
  sha256: crypto.createHash('sha256').update(buffer).digest('hex'),
584
592
  };
585
593
  }
586
594
 
595
+ function buildCandidateEnvironmentBindings(state) {
596
+ const targets =
597
+ state?.targets && typeof state.targets === 'object'
598
+ ? state.targets
599
+ : {};
600
+ return Object.fromEntries(
601
+ Object.entries(targets)
602
+ .sort(([left], [right]) => left.localeCompare(right))
603
+ .map(([targetName, binding]) => [
604
+ targetName,
605
+ {
606
+ targetName,
607
+ profile: binding?.profile || null,
608
+ environmentId: binding?.environmentId || null,
609
+ kind: binding?.kind || null,
610
+ appType: binding?.appType || null,
611
+ publicOrigin: binding?.publicOrigin || null,
612
+ sideEffectPolicy:
613
+ cloneJsonValue(binding?.sideEffectPolicy || {}) || {},
614
+ revision:
615
+ binding?.revision === undefined
616
+ ? null
617
+ : binding.revision,
618
+ resources: cloneJsonValue(binding?.resources || {}) || {},
619
+ },
620
+ ])
621
+ );
622
+ }
623
+
624
+ function candidateBindingValueContains(expected, current) {
625
+ if (Array.isArray(expected)) {
626
+ return (
627
+ Array.isArray(current) &&
628
+ expected.length === current.length &&
629
+ expected.every((item, index) =>
630
+ candidateBindingValueContains(item, current[index])
631
+ )
632
+ );
633
+ }
634
+ if (expected && typeof expected === 'object') {
635
+ if (!current || typeof current !== 'object' || Array.isArray(current)) {
636
+ return false;
637
+ }
638
+ return Object.entries(expected).every(([key, value]) =>
639
+ candidateBindingValueContains(value, current[key])
640
+ );
641
+ }
642
+ return expected === current;
643
+ }
644
+
645
+ function inspectCandidateEnvironmentBindings(candidate, state) {
646
+ const expected =
647
+ candidate?.sourceBundleManifest?.environmentBindings ||
648
+ candidate?.environmentBindings ||
649
+ {};
650
+ const current = buildCandidateEnvironmentBindings(state);
651
+ const mismatches = [];
652
+ const expectedNames = Object.keys(expected).sort();
653
+ const currentNames = Object.keys(current).sort();
654
+ if (canonicalJson(expectedNames) !== canonicalJson(currentNames)) {
655
+ mismatches.push({
656
+ targetName: '(targets)',
657
+ reason: 'target-set-changed',
658
+ expected: expectedNames,
659
+ current: currentNames,
660
+ });
661
+ }
662
+ for (const targetName of expectedNames) {
663
+ const expectedBinding = expected[targetName];
664
+ const currentBinding = current[targetName];
665
+ if (!currentBinding) continue;
666
+ for (const field of [
667
+ 'targetName',
668
+ 'profile',
669
+ 'environmentId',
670
+ 'kind',
671
+ 'appType',
672
+ 'publicOrigin',
673
+ 'sideEffectPolicy',
674
+ 'revision',
675
+ ]) {
676
+ if (
677
+ canonicalJson(expectedBinding?.[field]) !==
678
+ canonicalJson(currentBinding?.[field])
679
+ ) {
680
+ mismatches.push({
681
+ targetName,
682
+ field,
683
+ reason: 'changed',
684
+ expected: expectedBinding?.[field],
685
+ current: currentBinding?.[field],
686
+ });
687
+ }
688
+ }
689
+ if (
690
+ !candidateBindingValueContains(
691
+ expectedBinding?.resources || {},
692
+ currentBinding.resources || {}
693
+ )
694
+ ) {
695
+ mismatches.push({
696
+ targetName,
697
+ field: 'resources',
698
+ reason: 'sealed-resource-binding-changed',
699
+ expected: expectedBinding?.resources || {},
700
+ current: currentBinding.resources || {},
701
+ });
702
+ }
703
+ }
704
+ return {
705
+ valid: mismatches.length === 0,
706
+ checked: expectedNames.length,
707
+ mismatches,
708
+ };
709
+ }
710
+
711
+ function assertCandidateEnvironmentBindingsCurrent(candidate, state) {
712
+ const inspection = inspectCandidateEnvironmentBindings(candidate, state);
713
+ if (inspection.valid) return inspection;
714
+ const error = new Error(
715
+ `CANDIDATE_ENVIRONMENT_BINDINGS_CHANGED: sealed candidate 的环境或资源绑定已变化: ${inspection.mismatches
716
+ .slice(0, 8)
717
+ .map(
718
+ item =>
719
+ `${item.targetName}${
720
+ item.field ? `.${item.field}` : ''
721
+ }(${item.reason})`
722
+ )
723
+ .join(', ')}`
724
+ );
725
+ error.code = 'CANDIDATE_ENVIRONMENT_BINDINGS_CHANGED';
726
+ error.details = inspection;
727
+ throw error;
728
+ }
729
+
587
730
  function buildCandidateBundle(input) {
588
731
  const cwd = path.resolve(input.cwd || process.cwd());
589
732
  const files = Array.from(new Set(input.files || []))
@@ -610,10 +753,17 @@ function buildCandidateBundle(input) {
610
753
  sourceRevision: input.sourceRevision,
611
754
  runtimeMode: input.runtimeMode,
612
755
  targets: input.targets,
756
+ inputPolicy: 'candidate-inputs-v2',
613
757
  files,
758
+ environmentBindings:
759
+ cloneJsonValue(input.environmentBindings || {}) || {},
760
+ runtimeArtifacts:
761
+ cloneJsonValue(input.runtimeArtifacts || []) || [],
614
762
  testPlanHash: sha256Canonical(testPlan),
615
763
  prepublishVerificationHash:
616
764
  input.prepublishVerificationHash || null,
765
+ candidatePreflightHash:
766
+ input.candidatePreflightHash || null,
617
767
  openxiangdaVersion: input.openxiangdaVersion,
618
768
  };
619
769
  return {
@@ -624,6 +774,75 @@ function buildCandidateBundle(input) {
624
774
  };
625
775
  }
626
776
 
777
+ function inspectCandidateBundleFiles(candidate, cwd = process.cwd()) {
778
+ const manifest =
779
+ candidate?.sourceBundleManifest &&
780
+ typeof candidate.sourceBundleManifest === 'object'
781
+ ? candidate.sourceBundleManifest
782
+ : candidate;
783
+ const expectedFiles = Array.isArray(manifest?.files)
784
+ ? manifest.files
785
+ : [];
786
+ const mismatches = [];
787
+ const seen = new Set();
788
+ for (const expected of expectedFiles) {
789
+ const file = String(expected?.path || '').replace(/\\/g, '/');
790
+ if (!file || seen.has(file)) {
791
+ mismatches.push({
792
+ path: file || '(missing path)',
793
+ reason: seen.has(file) ? 'duplicate' : 'invalid',
794
+ });
795
+ continue;
796
+ }
797
+ seen.add(file);
798
+ const current = hashWorkspaceFile(path.resolve(cwd), file);
799
+ const expectedExists = expected.exists !== false;
800
+ if (!current || (expectedExists && current.exists === false)) {
801
+ mismatches.push({ path: file, reason: 'missing' });
802
+ continue;
803
+ }
804
+ if (!expectedExists) {
805
+ if (current.exists !== false) {
806
+ mismatches.push({ path: file, reason: 'reappeared', current });
807
+ }
808
+ continue;
809
+ }
810
+ if (
811
+ current.size !== Number(expected.size) ||
812
+ current.sha256 !== String(expected.sha256 || '').toLowerCase()
813
+ ) {
814
+ mismatches.push({
815
+ path: file,
816
+ reason: 'changed',
817
+ expected: {
818
+ size: Number(expected.size),
819
+ sha256: String(expected.sha256 || '').toLowerCase(),
820
+ },
821
+ current,
822
+ });
823
+ }
824
+ }
825
+ return {
826
+ valid: mismatches.length === 0,
827
+ checked: expectedFiles.length,
828
+ mismatches,
829
+ };
830
+ }
831
+
832
+ function assertCandidateBundleFilesCurrent(candidate, cwd = process.cwd()) {
833
+ const inspection = inspectCandidateBundleFiles(candidate, cwd);
834
+ if (inspection.valid) return inspection;
835
+ const error = new Error(
836
+ `CANDIDATE_INPUTS_CHANGED: sealed candidate 的发布输入已变化: ${inspection.mismatches
837
+ .slice(0, 8)
838
+ .map(item => `${item.path}(${item.reason})`)
839
+ .join(', ')}`
840
+ );
841
+ error.code = 'CANDIDATE_INPUTS_CHANGED';
842
+ error.details = inspection;
843
+ throw error;
844
+ }
845
+
627
846
  function releaseExecutionPath(changeId, deploymentId, cwd = process.cwd()) {
628
847
  const base = path.join(
629
848
  cwd,
@@ -706,7 +925,10 @@ function isRecoverablePostActivationDeploymentError(error, deployment) {
706
925
  }
707
926
 
708
927
  module.exports = {
928
+ assertCandidateBundleFilesCurrent,
929
+ assertCandidateEnvironmentBindingsCurrent,
709
930
  bindEnvironmentTarget,
931
+ buildCandidateEnvironmentBindings,
710
932
  buildSideEffectPolicyDiff,
711
933
  buildCandidateBundle,
712
934
  canonicalJson,
@@ -718,6 +940,8 @@ module.exports = {
718
940
  normalizeSideEffectPolicyInput,
719
941
  normalizeTargetName,
720
942
  hasStateResourceMappings,
943
+ inspectCandidateBundleFiles,
944
+ inspectCandidateEnvironmentBindings,
721
945
  managedTargetDeploymentFile,
722
946
  readCandidate,
723
947
  readJsonInput,