@fission-ai/openspec 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/README.md +9 -6
  2. package/bin/openspec.js +3 -1
  3. package/dist/cli/index.d.ts +4 -1
  4. package/dist/cli/index.js +46 -2
  5. package/dist/commands/completion.js +7 -0
  6. package/dist/commands/config.js +64 -5
  7. package/dist/commands/context-store.d.ts +3 -0
  8. package/dist/commands/context-store.js +475 -0
  9. package/dist/commands/initiative.d.ts +13 -0
  10. package/dist/commands/initiative.js +318 -0
  11. package/dist/commands/workflow/index.d.ts +2 -0
  12. package/dist/commands/workflow/index.js +1 -0
  13. package/dist/commands/workflow/initiative-link.d.ts +24 -0
  14. package/dist/commands/workflow/initiative-link.js +47 -0
  15. package/dist/commands/workflow/instructions.d.ts +1 -1
  16. package/dist/commands/workflow/instructions.js +46 -83
  17. package/dist/commands/workflow/new-change.d.ts +6 -0
  18. package/dist/commands/workflow/new-change.js +114 -17
  19. package/dist/commands/workflow/set-change.d.ts +13 -0
  20. package/dist/commands/workflow/set-change.js +87 -0
  21. package/dist/commands/workflow/shared.d.ts +5 -3
  22. package/dist/commands/workflow/shared.js +6 -6
  23. package/dist/commands/workflow/status.js +24 -9
  24. package/dist/commands/workflow/templates.js +5 -4
  25. package/dist/commands/workspace/context-status.d.ts +4 -0
  26. package/dist/commands/workspace/context-status.js +59 -0
  27. package/dist/commands/workspace/open-target-selection.d.ts +13 -0
  28. package/dist/commands/workspace/open-target-selection.js +146 -0
  29. package/dist/commands/workspace/open-view.d.ts +62 -0
  30. package/dist/commands/workspace/open-view.js +249 -0
  31. package/dist/commands/workspace/open.d.ts +37 -0
  32. package/dist/commands/workspace/open.js +110 -0
  33. package/dist/commands/workspace/opener-selection.d.ts +11 -0
  34. package/dist/commands/workspace/opener-selection.js +98 -0
  35. package/dist/commands/workspace/operations.d.ts +29 -0
  36. package/dist/commands/workspace/operations.js +543 -0
  37. package/dist/commands/workspace/prompt-theme.d.ts +29 -0
  38. package/dist/commands/workspace/prompt-theme.js +24 -0
  39. package/dist/commands/workspace/registration.d.ts +13 -0
  40. package/dist/commands/workspace/registration.js +84 -0
  41. package/dist/commands/workspace/selection.d.ts +9 -0
  42. package/dist/commands/workspace/selection.js +115 -0
  43. package/dist/commands/workspace/setup-prompts.d.ts +13 -0
  44. package/dist/commands/workspace/setup-prompts.js +121 -0
  45. package/dist/commands/workspace/types.d.ts +103 -0
  46. package/dist/commands/workspace/types.js +36 -0
  47. package/dist/commands/workspace.d.ts +6 -0
  48. package/dist/commands/workspace.js +587 -0
  49. package/dist/core/artifact-graph/index.d.ts +3 -1
  50. package/dist/core/artifact-graph/index.js +1 -0
  51. package/dist/core/artifact-graph/instruction-loader.d.ts +41 -1
  52. package/dist/core/artifact-graph/instruction-loader.js +51 -9
  53. package/dist/core/artifact-graph/outputs.d.ts +14 -0
  54. package/dist/core/artifact-graph/outputs.js +39 -0
  55. package/dist/core/artifact-graph/state.js +2 -25
  56. package/dist/core/artifact-graph/types.d.ts +0 -5
  57. package/dist/core/artifact-graph/types.js +0 -14
  58. package/dist/core/change-metadata/index.d.ts +2 -0
  59. package/dist/core/change-metadata/index.js +2 -0
  60. package/dist/core/change-metadata/schema.d.ts +18 -0
  61. package/dist/core/change-metadata/schema.js +28 -0
  62. package/dist/core/change-status-policy.d.ts +50 -0
  63. package/dist/core/change-status-policy.js +70 -0
  64. package/dist/core/collections/index.d.ts +3 -0
  65. package/dist/core/collections/index.js +3 -0
  66. package/dist/core/collections/initiatives/collection.d.ts +4 -0
  67. package/dist/core/collections/initiatives/collection.js +17 -0
  68. package/dist/core/collections/initiatives/index.d.ts +6 -0
  69. package/dist/core/collections/initiatives/index.js +6 -0
  70. package/dist/core/collections/initiatives/operations.d.ts +49 -0
  71. package/dist/core/collections/initiatives/operations.js +175 -0
  72. package/dist/core/collections/initiatives/resolution.d.ts +87 -0
  73. package/dist/core/collections/initiatives/resolution.js +374 -0
  74. package/dist/core/collections/initiatives/schema.d.ts +41 -0
  75. package/dist/core/collections/initiatives/schema.js +134 -0
  76. package/dist/core/collections/initiatives/templates.d.ts +12 -0
  77. package/dist/core/collections/initiatives/templates.js +90 -0
  78. package/dist/core/collections/runtime.d.ts +46 -0
  79. package/dist/core/collections/runtime.js +194 -0
  80. package/dist/core/completions/command-registry.d.ts +1 -5
  81. package/dist/core/completions/command-registry.js +532 -32
  82. package/dist/core/completions/completion-provider.d.ts +11 -0
  83. package/dist/core/completions/completion-provider.js +27 -0
  84. package/dist/core/completions/generators/bash-generator.d.ts +3 -0
  85. package/dist/core/completions/generators/bash-generator.js +60 -4
  86. package/dist/core/completions/generators/fish-generator.js +3 -0
  87. package/dist/core/completions/generators/powershell-generator.d.ts +3 -0
  88. package/dist/core/completions/generators/powershell-generator.js +63 -4
  89. package/dist/core/completions/generators/zsh-generator.d.ts +3 -0
  90. package/dist/core/completions/generators/zsh-generator.js +46 -22
  91. package/dist/core/completions/installers/zsh-installer.d.ts +0 -8
  92. package/dist/core/completions/installers/zsh-installer.js +3 -31
  93. package/dist/core/completions/shared-flags.d.ts +12 -0
  94. package/dist/core/completions/shared-flags.js +28 -0
  95. package/dist/core/completions/templates/bash-templates.d.ts +1 -1
  96. package/dist/core/completions/templates/bash-templates.js +6 -0
  97. package/dist/core/completions/templates/fish-templates.d.ts +1 -1
  98. package/dist/core/completions/templates/fish-templates.js +6 -0
  99. package/dist/core/completions/templates/powershell-templates.d.ts +1 -1
  100. package/dist/core/completions/templates/powershell-templates.js +9 -0
  101. package/dist/core/completions/templates/zsh-templates.d.ts +1 -1
  102. package/dist/core/completions/templates/zsh-templates.js +9 -0
  103. package/dist/core/completions/types.d.ts +23 -1
  104. package/dist/core/config.js +3 -1
  105. package/dist/core/context-store/binding.d.ts +53 -0
  106. package/dist/core/context-store/binding.js +197 -0
  107. package/dist/core/context-store/errors.d.ts +20 -0
  108. package/dist/core/context-store/errors.js +22 -0
  109. package/dist/core/context-store/foundation.d.ts +55 -0
  110. package/dist/core/context-store/foundation.js +321 -0
  111. package/dist/core/context-store/index.d.ts +6 -0
  112. package/dist/core/context-store/index.js +6 -0
  113. package/dist/core/context-store/operations.d.ts +85 -0
  114. package/dist/core/context-store/operations.js +528 -0
  115. package/dist/core/context-store/registry.d.ts +45 -0
  116. package/dist/core/context-store/registry.js +229 -0
  117. package/dist/core/global-config.d.ts +6 -1
  118. package/dist/core/global-config.js +15 -16
  119. package/dist/core/index.d.ts +5 -1
  120. package/dist/core/index.js +4 -0
  121. package/dist/core/parsers/change-parser.js +7 -3
  122. package/dist/core/parsers/markdown-parser.d.ts +4 -0
  123. package/dist/core/parsers/markdown-parser.js +41 -1
  124. package/dist/core/parsers/requirement-blocks.js +5 -5
  125. package/dist/core/parsers/spec-structure.d.ts +9 -0
  126. package/dist/core/parsers/spec-structure.js +88 -0
  127. package/dist/core/planning-home.d.ts +21 -0
  128. package/dist/core/planning-home.js +108 -0
  129. package/dist/core/profiles.d.ts +1 -1
  130. package/dist/core/profiles.js +1 -1
  131. package/dist/core/specs-apply.js +9 -1
  132. package/dist/core/templates/workflows/apply-change.js +10 -4
  133. package/dist/core/templates/workflows/archive-change.js +21 -15
  134. package/dist/core/templates/workflows/bulk-archive-change.js +14 -10
  135. package/dist/core/templates/workflows/continue-change.js +6 -4
  136. package/dist/core/templates/workflows/explore.js +8 -10
  137. package/dist/core/templates/workflows/ff-change.js +8 -6
  138. package/dist/core/templates/workflows/new-change.js +6 -6
  139. package/dist/core/templates/workflows/onboard.js +13 -15
  140. package/dist/core/templates/workflows/propose.js +8 -6
  141. package/dist/core/templates/workflows/sync-specs.js +28 -10
  142. package/dist/core/templates/workflows/verify-change.js +16 -10
  143. package/dist/core/update.d.ts +5 -0
  144. package/dist/core/update.js +20 -0
  145. package/dist/core/validation/validator.d.ts +11 -0
  146. package/dist/core/validation/validator.js +28 -2
  147. package/dist/core/workspace/foundation.d.ts +67 -0
  148. package/dist/core/workspace/foundation.js +295 -0
  149. package/dist/core/workspace/index.d.ts +8 -0
  150. package/dist/core/workspace/index.js +8 -0
  151. package/dist/core/workspace/legacy-state.d.ts +28 -0
  152. package/dist/core/workspace/legacy-state.js +200 -0
  153. package/dist/core/workspace/link-input.d.ts +9 -0
  154. package/dist/core/workspace/link-input.js +32 -0
  155. package/dist/core/workspace/open-surface.d.ts +45 -0
  156. package/dist/core/workspace/open-surface.js +215 -0
  157. package/dist/core/workspace/openers.d.ts +21 -0
  158. package/dist/core/workspace/openers.js +124 -0
  159. package/dist/core/workspace/registry.d.ts +24 -0
  160. package/dist/core/workspace/registry.js +146 -0
  161. package/dist/core/workspace/skills.d.ts +57 -0
  162. package/dist/core/workspace/skills.js +334 -0
  163. package/dist/core/workspace/state-io.d.ts +10 -0
  164. package/dist/core/workspace/state-io.js +119 -0
  165. package/dist/telemetry/config.d.ts +7 -1
  166. package/dist/telemetry/config.js +87 -19
  167. package/dist/telemetry/index.js +19 -0
  168. package/dist/utils/change-metadata.d.ts +5 -2
  169. package/dist/utils/change-metadata.js +7 -13
  170. package/dist/utils/change-utils.d.ts +9 -0
  171. package/dist/utils/change-utils.js +7 -5
  172. package/dist/utils/file-system.d.ts +5 -0
  173. package/dist/utils/file-system.js +21 -1
  174. package/package.json +2 -1
  175. package/schemas/workspace-planning/schema.yaml +72 -0
  176. package/schemas/workspace-planning/templates/design.md +33 -0
  177. package/schemas/workspace-planning/templates/proposal.md +28 -0
  178. package/schemas/workspace-planning/templates/spec.md +9 -0
  179. package/schemas/workspace-planning/templates/tasks.md +15 -0
@@ -0,0 +1,229 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import { getContextStoreMetadataPath, getContextStoreMetadataDir, listContextStoreRegistryEntries, readContextStoreRegistryState, readOptionalContextStoreMetadataState, resolveGitContextStoreBackendConfig, updateContextStoreRegistryState, validateContextStoreId, writeContextStoreMetadataState, } from './foundation.js';
3
+ import { ContextStoreError } from './errors.js';
4
+ import { FileSystemUtils } from '../../utils/file-system.js';
5
+ export function getStoreRootForBackend(backend) {
6
+ switch (backend.type) {
7
+ case 'git':
8
+ return backend.local_path;
9
+ }
10
+ }
11
+ function normalizePathForComparison(targetPath) {
12
+ try {
13
+ return FileSystemUtils.canonicalizeExistingPath(targetPath);
14
+ }
15
+ catch {
16
+ return targetPath;
17
+ }
18
+ }
19
+ export function assertNoRegisteredStoreConflict(registry, id, backend) {
20
+ const nextPath = normalizePathForComparison(getStoreRootForBackend(backend));
21
+ for (const entry of listContextStoreRegistryEntries(registry ?? { version: 1, stores: {} })) {
22
+ const entryPath = normalizePathForComparison(getStoreRootForBackend(entry.backend));
23
+ if (entry.id === id && entryPath === nextPath) {
24
+ continue;
25
+ }
26
+ if (entry.id === id) {
27
+ throw new ContextStoreError(`Context store '${id}' is already registered at ${getStoreRootForBackend(entry.backend)}.`, 'context_store_id_conflict', {
28
+ target: 'context_store.id',
29
+ fix: 'Use the existing registration or choose a different context store id.',
30
+ });
31
+ }
32
+ if (entryPath === nextPath) {
33
+ throw new ContextStoreError(`Context store path is already registered as '${entry.id}'.`, 'context_store_path_conflict', {
34
+ target: 'context_store.root',
35
+ fix: `Use the existing '${entry.id}' registration or choose a different path.`,
36
+ });
37
+ }
38
+ }
39
+ }
40
+ function withRegisteredStore(registry, id, backend) {
41
+ assertNoRegisteredStoreConflict(registry, id, backend);
42
+ const stores = {
43
+ ...(registry?.stores ?? {}),
44
+ [id]: {
45
+ backend,
46
+ },
47
+ };
48
+ return {
49
+ version: 1,
50
+ stores: Object.fromEntries(Object.entries(stores).sort(([leftId], [rightId]) => leftId.localeCompare(rightId))),
51
+ };
52
+ }
53
+ function getRegisteredStoreOrThrow(registry, id) {
54
+ const entry = registry?.stores[id];
55
+ if (!entry) {
56
+ throw new ContextStoreError(`Unknown context store '${id}'`, 'context_store_not_found', {
57
+ target: 'context_store.id',
58
+ fix: 'Run openspec context-store list to see registered stores.',
59
+ });
60
+ }
61
+ return {
62
+ id,
63
+ backend: entry.backend,
64
+ };
65
+ }
66
+ function contextStoreBackendsMatch(actual, expected) {
67
+ return (actual.type === expected.type &&
68
+ normalizePathForComparison(actual.local_path) ===
69
+ normalizePathForComparison(expected.local_path) &&
70
+ actual.remote === expected.remote &&
71
+ actual.branch === expected.branch);
72
+ }
73
+ function assertExpectedRegisteredBackend(id, actual, expected) {
74
+ if (!expected || contextStoreBackendsMatch(actual, expected))
75
+ return;
76
+ throw new ContextStoreError(`Context store '${id}' changed before cleanup completed.`, 'context_store_registry_changed', {
77
+ target: 'context_store.registry',
78
+ fix: 'Retry the cleanup command after reviewing the current context-store registration.',
79
+ });
80
+ }
81
+ function withoutRegisteredStore(registry, id, expectedBackend) {
82
+ const removed = getRegisteredStoreOrThrow(registry, id);
83
+ assertExpectedRegisteredBackend(id, removed.backend, expectedBackend);
84
+ const stores = { ...(registry?.stores ?? {}) };
85
+ delete stores[id];
86
+ return {
87
+ removed,
88
+ next: {
89
+ version: 1,
90
+ stores: Object.fromEntries(Object.entries(stores).sort(([leftId], [rightId]) => leftId.localeCompare(rightId))),
91
+ },
92
+ };
93
+ }
94
+ async function ensureStoreMetadata(storeRoot, id, options) {
95
+ const metadata = await readOptionalContextStoreMetadataState(storeRoot);
96
+ if (!metadata) {
97
+ if (!options.writeIfMissing) {
98
+ throw new ContextStoreError(`Registered context store '${id}' is missing metadata at ${getContextStoreMetadataPath(storeRoot)}`, 'context_store_metadata_missing', {
99
+ target: 'context_store.metadata',
100
+ fix: `Create ${getContextStoreMetadataPath(storeRoot)} or rerun context-store register.`,
101
+ });
102
+ }
103
+ await writeContextStoreMetadataState(storeRoot, {
104
+ version: 1,
105
+ id,
106
+ });
107
+ return true;
108
+ }
109
+ if (metadata.id !== id) {
110
+ throw new ContextStoreError(`Context store metadata id '${metadata.id}' does not match registered id '${id}'`, 'context_store_metadata_id_mismatch', {
111
+ target: 'context_store.metadata',
112
+ fix: 'Repair the local registry or store metadata so the ids match.',
113
+ });
114
+ }
115
+ return false;
116
+ }
117
+ export async function commitContextStoreRegistration(input) {
118
+ const id = validateContextStoreId(input.id);
119
+ const backend = input.backend;
120
+ const storeRoot = getStoreRootForBackend(backend);
121
+ let metadataCreated = false;
122
+ try {
123
+ metadataCreated = await ensureStoreMetadata(storeRoot, id, {
124
+ writeIfMissing: input.writeMetadataIfMissing,
125
+ });
126
+ await updateContextStoreRegistryState((registry) => withRegisteredStore(registry, id, backend), { globalDataDir: input.globalDataDir });
127
+ }
128
+ catch (error) {
129
+ if (metadataCreated) {
130
+ await fs.rm(getContextStoreMetadataPath(storeRoot), { force: true });
131
+ await fs.rmdir(getContextStoreMetadataDir(storeRoot)).catch(() => undefined);
132
+ }
133
+ throw error;
134
+ }
135
+ return {
136
+ id,
137
+ storeRoot,
138
+ backend,
139
+ metadataCreated,
140
+ };
141
+ }
142
+ export async function registerContextStore(input) {
143
+ const id = validateContextStoreId(input.id);
144
+ const backend = await resolveGitContextStoreBackendConfig({
145
+ localPath: input.localPath,
146
+ ...(input.remote !== undefined ? { remote: input.remote } : {}),
147
+ ...(input.branch !== undefined ? { branch: input.branch } : {}),
148
+ }, input.cwd);
149
+ const storeRoot = getStoreRootForBackend(backend);
150
+ const committed = await commitContextStoreRegistration({
151
+ id,
152
+ backend,
153
+ writeMetadataIfMissing: true,
154
+ ...(input.globalDataDir ? { globalDataDir: input.globalDataDir } : {}),
155
+ });
156
+ return {
157
+ id: committed.id,
158
+ storeRoot: committed.storeRoot,
159
+ backend: committed.backend,
160
+ };
161
+ }
162
+ export async function listRegisteredContextStores(options = {}) {
163
+ const registry = await readContextStoreRegistryState(options);
164
+ if (!registry) {
165
+ return [];
166
+ }
167
+ return listContextStoreRegistryEntries(registry).map((entry) => ({
168
+ ...entry,
169
+ storeRoot: getStoreRootForBackend(entry.backend),
170
+ }));
171
+ }
172
+ export async function getRegisteredContextStore(input) {
173
+ const id = validateContextStoreId(input.id);
174
+ const registry = await readContextStoreRegistryState({
175
+ globalDataDir: input.globalDataDir,
176
+ });
177
+ const entry = getRegisteredStoreOrThrow(registry, id);
178
+ assertExpectedRegisteredBackend(id, entry.backend, input.expectedBackend);
179
+ return {
180
+ ...entry,
181
+ storeRoot: getStoreRootForBackend(entry.backend),
182
+ };
183
+ }
184
+ export async function unregisterContextStoreRegistration(input) {
185
+ const id = validateContextStoreId(input.id);
186
+ let removed;
187
+ await updateContextStoreRegistryState(async (registry) => {
188
+ const result = withoutRegisteredStore(registry, id, input.expectedBackend);
189
+ const removedEntry = {
190
+ ...result.removed,
191
+ storeRoot: getStoreRootForBackend(result.removed.backend),
192
+ };
193
+ await input.beforeCommit?.(removedEntry);
194
+ removed = result.removed;
195
+ return result.next;
196
+ }, { globalDataDir: input.globalDataDir });
197
+ if (!removed) {
198
+ throw new ContextStoreError(`Unknown context store '${id}'`, 'context_store_not_found', {
199
+ target: 'context_store.id',
200
+ fix: 'Run openspec context-store list to see registered stores.',
201
+ });
202
+ }
203
+ return {
204
+ ...removed,
205
+ storeRoot: getStoreRootForBackend(removed.backend),
206
+ };
207
+ }
208
+ export async function resolveRegisteredContextStore(input) {
209
+ const id = validateContextStoreId(input.id);
210
+ const registry = await readContextStoreRegistryState({
211
+ globalDataDir: input.globalDataDir,
212
+ });
213
+ if (!registry) {
214
+ throw new ContextStoreError('No context store registry found', 'no_context_store_registry', {
215
+ target: 'context_store.id',
216
+ fix: 'Register a context store before using --store, or pass --store-path <path>.',
217
+ });
218
+ }
219
+ const entry = getRegisteredStoreOrThrow(registry, id);
220
+ const backend = entry.backend;
221
+ const storeRoot = getStoreRootForBackend(backend);
222
+ await ensureStoreMetadata(storeRoot, id, { writeIfMissing: false });
223
+ return {
224
+ id,
225
+ storeRoot,
226
+ backend,
227
+ };
228
+ }
229
+ //# sourceMappingURL=registry.js.map
@@ -25,7 +25,12 @@ export declare function getGlobalConfigDir(): string;
25
25
  * - Unix/macOS fallback: ~/.local/share/openspec/
26
26
  * - Windows fallback: %LOCALAPPDATA%/openspec/
27
27
  */
28
- export declare function getGlobalDataDir(): string;
28
+ export interface GlobalDataDirOptions {
29
+ env?: NodeJS.ProcessEnv;
30
+ platform?: NodeJS.Platform;
31
+ homedir?: string;
32
+ }
33
+ export declare function getGlobalDataDir(options?: GlobalDataDirOptions): string;
29
34
  /**
30
35
  * Gets the path to the global config file.
31
36
  */
@@ -36,32 +36,31 @@ export function getGlobalConfigDir() {
36
36
  // Unix/macOS fallback: ~/.config
37
37
  return path.join(os.homedir(), '.config', GLOBAL_CONFIG_DIR_NAME);
38
38
  }
39
- /**
40
- * Gets the global data directory path following XDG Base Directory Specification.
41
- * Used for user data like schema overrides.
42
- *
43
- * - All platforms: $XDG_DATA_HOME/openspec/ if XDG_DATA_HOME is set
44
- * - Unix/macOS fallback: ~/.local/share/openspec/
45
- * - Windows fallback: %LOCALAPPDATA%/openspec/
46
- */
47
- export function getGlobalDataDir() {
39
+ function joinGlobalDataPath(platform, ...segments) {
40
+ return platform === 'win32'
41
+ ? path.win32.join(...segments)
42
+ : path.posix.join(...segments);
43
+ }
44
+ export function getGlobalDataDir(options = {}) {
45
+ const env = options.env ?? process.env;
46
+ const platform = options.platform ?? os.platform();
48
47
  // XDG_DATA_HOME takes precedence on all platforms when explicitly set
49
- const xdgDataHome = process.env.XDG_DATA_HOME;
48
+ const xdgDataHome = env.XDG_DATA_HOME;
50
49
  if (xdgDataHome) {
51
- return path.join(xdgDataHome, GLOBAL_DATA_DIR_NAME);
50
+ return joinGlobalDataPath(platform, xdgDataHome, GLOBAL_DATA_DIR_NAME);
52
51
  }
53
- const platform = os.platform();
52
+ const homedir = options.homedir ?? os.homedir();
54
53
  if (platform === 'win32') {
55
54
  // Windows: use %LOCALAPPDATA%
56
- const localAppData = process.env.LOCALAPPDATA;
55
+ const localAppData = env.LOCALAPPDATA;
57
56
  if (localAppData) {
58
- return path.join(localAppData, GLOBAL_DATA_DIR_NAME);
57
+ return joinGlobalDataPath(platform, localAppData, GLOBAL_DATA_DIR_NAME);
59
58
  }
60
59
  // Fallback for Windows if LOCALAPPDATA is not set
61
- return path.join(os.homedir(), 'AppData', 'Local', GLOBAL_DATA_DIR_NAME);
60
+ return joinGlobalDataPath(platform, homedir, 'AppData', 'Local', GLOBAL_DATA_DIR_NAME);
62
61
  }
63
62
  // Unix/macOS fallback: ~/.local/share
64
- return path.join(os.homedir(), '.local', 'share', GLOBAL_DATA_DIR_NAME);
63
+ return joinGlobalDataPath(platform, homedir, '.local', 'share', GLOBAL_DATA_DIR_NAME);
65
64
  }
66
65
  /**
67
66
  * Gets the path to the global config file.
@@ -1,2 +1,6 @@
1
- export { GLOBAL_CONFIG_DIR_NAME, GLOBAL_CONFIG_FILE_NAME, GLOBAL_DATA_DIR_NAME, type GlobalConfig, getGlobalConfigDir, getGlobalConfigPath, getGlobalConfig, saveGlobalConfig, getGlobalDataDir } from './global-config.js';
1
+ export { GLOBAL_CONFIG_DIR_NAME, GLOBAL_CONFIG_FILE_NAME, GLOBAL_DATA_DIR_NAME, type GlobalDataDirOptions, type GlobalConfig, getGlobalConfigDir, getGlobalConfigPath, getGlobalConfig, saveGlobalConfig, getGlobalDataDir } from './global-config.js';
2
+ export * from './workspace/index.js';
3
+ export * from './context-store/index.js';
4
+ export * from './collections/index.js';
5
+ export * from './planning-home.js';
2
6
  //# sourceMappingURL=index.d.ts.map
@@ -1,3 +1,7 @@
1
1
  // Core OpenSpec logic will be implemented here
2
2
  export { GLOBAL_CONFIG_DIR_NAME, GLOBAL_CONFIG_FILE_NAME, GLOBAL_DATA_DIR_NAME, getGlobalConfigDir, getGlobalConfigPath, getGlobalConfig, saveGlobalConfig, getGlobalDataDir } from './global-config.js';
3
+ export * from './workspace/index.js';
4
+ export * from './context-store/index.js';
5
+ export * from './collections/index.js';
6
+ export * from './planning-home.js';
3
7
  //# sourceMappingURL=index.js.map
@@ -148,15 +148,19 @@ export class ChangeParser extends MarkdownParser {
148
148
  parseSectionsFromContent(content) {
149
149
  const normalizedContent = ChangeParser.normalizeContent(content);
150
150
  const lines = normalizedContent.split('\n');
151
+ const codeFenceLineMask = ChangeParser.buildCodeFenceMask(lines);
151
152
  const sections = [];
152
153
  const stack = [];
153
154
  for (let i = 0; i < lines.length; i++) {
154
155
  const line = lines[i];
156
+ if (codeFenceLineMask[i]) {
157
+ continue;
158
+ }
155
159
  const headerMatch = line.match(/^(#{1,6})\s+(.+)$/);
156
160
  if (headerMatch) {
157
161
  const level = headerMatch[1].length;
158
162
  const title = headerMatch[2].trim();
159
- const contentLines = this.getContentUntilNextHeaderFromLines(lines, i + 1, level);
163
+ const contentLines = this.getContentUntilNextHeaderFromLines(lines, codeFenceLineMask, i + 1, level);
160
164
  const section = {
161
165
  level,
162
166
  title,
@@ -177,11 +181,11 @@ export class ChangeParser extends MarkdownParser {
177
181
  }
178
182
  return sections;
179
183
  }
180
- getContentUntilNextHeaderFromLines(lines, startLine, currentLevel) {
184
+ getContentUntilNextHeaderFromLines(lines, codeFenceLineMask, startLine, currentLevel) {
181
185
  const contentLines = [];
182
186
  for (let i = startLine; i < lines.length; i++) {
183
187
  const line = lines[i];
184
- const headerMatch = line.match(/^(#{1,6})\s+/);
188
+ const headerMatch = codeFenceLineMask[i] ? null : line.match(/^(#{1,6})\s+/);
185
189
  if (headerMatch && headerMatch[1].length <= currentLevel) {
186
190
  break;
187
191
  }
@@ -7,9 +7,13 @@ export interface Section {
7
7
  }
8
8
  export declare class MarkdownParser {
9
9
  private lines;
10
+ private codeFenceLineMask;
10
11
  private currentLine;
11
12
  constructor(content: string);
12
13
  protected static normalizeContent(content: string): string;
14
+ protected static buildCodeFenceMask(lines: string[]): boolean[];
15
+ private static getFenceMarker;
16
+ private static isClosingFence;
13
17
  parseSpec(name: string): Spec;
14
18
  parseChange(name: string): Change;
15
19
  protected parseSections(): Section[];
@@ -1,14 +1,51 @@
1
1
  export class MarkdownParser {
2
2
  lines;
3
+ codeFenceLineMask;
3
4
  currentLine;
4
5
  constructor(content) {
5
6
  const normalized = MarkdownParser.normalizeContent(content);
6
7
  this.lines = normalized.split('\n');
8
+ this.codeFenceLineMask = MarkdownParser.buildCodeFenceMask(this.lines);
7
9
  this.currentLine = 0;
8
10
  }
9
11
  static normalizeContent(content) {
10
12
  return content.replace(/\r\n?/g, '\n');
11
13
  }
14
+ static buildCodeFenceMask(lines) {
15
+ const mask = new Array(lines.length).fill(false);
16
+ let activeFence = null;
17
+ for (let i = 0; i < lines.length; i++) {
18
+ const fence = MarkdownParser.getFenceMarker(lines[i]);
19
+ if (!activeFence) {
20
+ if (fence) {
21
+ activeFence = fence;
22
+ mask[i] = true;
23
+ }
24
+ continue;
25
+ }
26
+ mask[i] = true;
27
+ if (MarkdownParser.isClosingFence(lines[i], activeFence)) {
28
+ activeFence = null;
29
+ }
30
+ }
31
+ return mask;
32
+ }
33
+ static getFenceMarker(line) {
34
+ const fenceMatch = line.match(/^\s*(`{3,}|~{3,})/);
35
+ if (!fenceMatch) {
36
+ return null;
37
+ }
38
+ return {
39
+ marker: fenceMatch[1][0],
40
+ length: fenceMatch[1].length,
41
+ };
42
+ }
43
+ static isClosingFence(line, activeFence) {
44
+ const fenceMatch = line.match(/^\s*(`{3,}|~{3,})\s*$/);
45
+ return Boolean(fenceMatch &&
46
+ fenceMatch[1][0] === activeFence.marker &&
47
+ fenceMatch[1].length >= activeFence.length);
48
+ }
12
49
  parseSpec(name) {
13
50
  const sections = this.parseSections();
14
51
  const purpose = this.findSection(sections, 'Purpose')?.content || '';
@@ -57,6 +94,9 @@ export class MarkdownParser {
57
94
  const stack = [];
58
95
  for (let i = 0; i < this.lines.length; i++) {
59
96
  const line = this.lines[i];
97
+ if (this.codeFenceLineMask[i]) {
98
+ continue;
99
+ }
60
100
  const headerMatch = line.match(/^(#{1,6})\s+(.+)$/);
61
101
  if (headerMatch) {
62
102
  const level = headerMatch[1].length;
@@ -86,7 +126,7 @@ export class MarkdownParser {
86
126
  const contentLines = [];
87
127
  for (let i = startLine; i < this.lines.length; i++) {
88
128
  const line = this.lines[i];
89
- const headerMatch = line.match(/^(#{1,6})\s+/);
129
+ const headerMatch = this.codeFenceLineMask[i] ? null : line.match(/^(#{1,6})\s+/);
90
130
  if (headerMatch && headerMatch[1].length <= currentLevel) {
91
131
  break;
92
132
  }
@@ -1,7 +1,7 @@
1
1
  export function normalizeRequirementName(name) {
2
2
  return name.trim();
3
3
  }
4
- const REQUIREMENT_HEADER_REGEX = /^###\s*Requirement:\s*(.+)\s*$/;
4
+ const REQUIREMENT_HEADER_REGEX = /^###\s*Requirement:\s*(.+)\s*$/i;
5
5
  /**
6
6
  * Extracts the Requirements section from a spec file and parses requirement blocks.
7
7
  */
@@ -37,7 +37,7 @@ export function extractRequirementsSection(content) {
37
37
  let cursor = 0;
38
38
  let preambleLines = [];
39
39
  // Collect preamble lines until first requirement header
40
- while (cursor < sectionBodyLines.length && !/^###\s+Requirement:/.test(sectionBodyLines[cursor])) {
40
+ while (cursor < sectionBodyLines.length && !REQUIREMENT_HEADER_REGEX.test(sectionBodyLines[cursor])) {
41
41
  preambleLines.push(sectionBodyLines[cursor]);
42
42
  cursor++;
43
43
  }
@@ -54,7 +54,7 @@ export function extractRequirementsSection(content) {
54
54
  cursor++;
55
55
  // Gather lines until next requirement header or end of section
56
56
  const bodyLines = [headerLineCandidate];
57
- while (cursor < sectionBodyLines.length && !/^###\s+Requirement:/.test(sectionBodyLines[cursor]) && !/^##\s+/.test(sectionBodyLines[cursor])) {
57
+ while (cursor < sectionBodyLines.length && !REQUIREMENT_HEADER_REGEX.test(sectionBodyLines[cursor]) && !/^##\s+/.test(sectionBodyLines[cursor])) {
58
58
  bodyLines.push(sectionBodyLines[cursor]);
59
59
  cursor++;
60
60
  }
@@ -136,7 +136,7 @@ function parseRequirementBlocksFromSection(sectionBody) {
136
136
  let i = 0;
137
137
  while (i < lines.length) {
138
138
  // Seek next requirement header
139
- while (i < lines.length && !/^###\s+Requirement:/.test(lines[i]))
139
+ while (i < lines.length && !REQUIREMENT_HEADER_REGEX.test(lines[i]))
140
140
  i++;
141
141
  if (i >= lines.length)
142
142
  break;
@@ -149,7 +149,7 @@ function parseRequirementBlocksFromSection(sectionBody) {
149
149
  const name = normalizeRequirementName(m[1]);
150
150
  const buf = [headerLine];
151
151
  i++;
152
- while (i < lines.length && !/^###\s+Requirement:/.test(lines[i]) && !/^##\s+/.test(lines[i])) {
152
+ while (i < lines.length && !REQUIREMENT_HEADER_REGEX.test(lines[i]) && !/^##\s+/.test(lines[i])) {
153
153
  buf.push(lines[i]);
154
154
  i++;
155
155
  }
@@ -0,0 +1,9 @@
1
+ export interface MainSpecStructureIssue {
2
+ kind: 'delta-header' | 'requirement-outside-requirements';
3
+ line: number;
4
+ header: string;
5
+ message: string;
6
+ }
7
+ export declare function findMainSpecStructureIssues(content: string): MainSpecStructureIssue[];
8
+ export declare function stripFencedCodeBlocksPreservingLines(content: string): string;
9
+ //# sourceMappingURL=spec-structure.d.ts.map
@@ -0,0 +1,88 @@
1
+ const REQUIREMENTS_SECTION_HEADER = /^##\s+Requirements\s*$/i;
2
+ const TOP_LEVEL_SECTION_HEADER = /^##\s+/;
3
+ const DELTA_HEADER = /^##\s+(ADDED|MODIFIED|REMOVED|RENAMED)\s+Requirements\s*$/i;
4
+ const REQUIREMENT_HEADER = /^###\s+Requirement:\s*(.+)\s*$/i;
5
+ export function findMainSpecStructureIssues(content) {
6
+ const normalized = content.replace(/\r\n?/g, '\n');
7
+ const stripped = stripFencedCodeBlocksPreservingLines(normalized);
8
+ const lines = stripped.split('\n');
9
+ const issues = [];
10
+ const requirementsHeaderIndex = lines.findIndex(line => REQUIREMENTS_SECTION_HEADER.test(line));
11
+ let requirementsEndIndex = lines.length;
12
+ if (requirementsHeaderIndex !== -1) {
13
+ for (let i = requirementsHeaderIndex + 1; i < lines.length; i++) {
14
+ if (TOP_LEVEL_SECTION_HEADER.test(lines[i])) {
15
+ requirementsEndIndex = i;
16
+ break;
17
+ }
18
+ }
19
+ }
20
+ for (let i = 0; i < lines.length; i++) {
21
+ const line = lines[i];
22
+ const trimmed = line.trim();
23
+ if (!trimmed) {
24
+ continue;
25
+ }
26
+ if (DELTA_HEADER.test(line)) {
27
+ issues.push({
28
+ kind: 'delta-header',
29
+ line: i + 1,
30
+ header: trimmed,
31
+ message: `Main spec contains delta header "${trimmed}". ` +
32
+ 'Delta headers are only valid inside openspec/changes/<name>/specs/<capability>/spec.md ' +
33
+ 'and truncate the parsed ## Requirements section.',
34
+ });
35
+ continue;
36
+ }
37
+ const requirementMatch = line.match(REQUIREMENT_HEADER);
38
+ if (!requirementMatch) {
39
+ continue;
40
+ }
41
+ const insideRequirements = requirementsHeaderIndex !== -1 &&
42
+ i > requirementsHeaderIndex &&
43
+ i < requirementsEndIndex;
44
+ if (!insideRequirements) {
45
+ issues.push({
46
+ kind: 'requirement-outside-requirements',
47
+ line: i + 1,
48
+ header: trimmed,
49
+ message: `Requirement header "${trimmed}" appears outside the main ## Requirements section. ` +
50
+ 'Main specs only parse requirements inside that section, so this requirement is currently invisible to validate, list, and archive.',
51
+ });
52
+ }
53
+ }
54
+ return issues;
55
+ }
56
+ export function stripFencedCodeBlocksPreservingLines(content) {
57
+ const lines = content.split('\n');
58
+ const output = [];
59
+ let activeFence = null;
60
+ for (const line of lines) {
61
+ const fenceMatch = line.match(/^\s*(`{3,}|~{3,})(.*)$/);
62
+ if (!activeFence) {
63
+ if (fenceMatch) {
64
+ activeFence = {
65
+ marker: fenceMatch[1][0],
66
+ length: fenceMatch[1].length,
67
+ };
68
+ output.push('');
69
+ }
70
+ else {
71
+ output.push(line);
72
+ }
73
+ continue;
74
+ }
75
+ output.push('');
76
+ if (isClosingFence(line, activeFence)) {
77
+ activeFence = null;
78
+ }
79
+ }
80
+ return output.join('\n');
81
+ }
82
+ function isClosingFence(line, activeFence) {
83
+ const fenceMatch = line.match(/^\s*(`{3,}|~{3,})\s*$/);
84
+ return Boolean(fenceMatch &&
85
+ fenceMatch[1][0] === activeFence.marker &&
86
+ fenceMatch[1].length >= activeFence.length);
87
+ }
88
+ //# sourceMappingURL=spec-structure.js.map
@@ -0,0 +1,21 @@
1
+ export type PlanningHomeKind = 'repo' | 'workspace';
2
+ export interface PlanningHome {
3
+ kind: PlanningHomeKind;
4
+ root: string;
5
+ changesDir: string;
6
+ defaultSchema: string;
7
+ workspace?: {
8
+ name: string;
9
+ links: string[];
10
+ };
11
+ }
12
+ export interface ResolvePlanningHomeOptions {
13
+ startPath?: string;
14
+ allowImplicitRepoRoot?: boolean;
15
+ }
16
+ export declare function findWorkspacePlanningRootSync(startPath?: string): string | null;
17
+ export declare function findRepoPlanningRootSync(startPath?: string): string | null;
18
+ export declare function resolveCurrentPlanningHomeSync(options?: ResolvePlanningHomeOptions): PlanningHome;
19
+ export declare function getChangeDir(planningHome: PlanningHome, changeName: string): string;
20
+ export declare function formatChangeLocation(planningHome: PlanningHome, changeName: string): string;
21
+ //# sourceMappingURL=planning-home.d.ts.map