@niujiajie/freespec 0.0.0-stage → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +58 -2
- package/bin/freespec.js +5 -0
- package/dist/cli/index.d.ts +25 -0
- package/dist/cli/index.js +359 -0
- package/dist/commands/shared-output.d.ts +18 -0
- package/dist/commands/shared-output.js +61 -0
- package/dist/commands/workflow/default-schema.d.ts +2 -0
- package/dist/commands/workflow/default-schema.js +4 -0
- package/dist/commands/workflow/index.d.ts +13 -0
- package/dist/commands/workflow/index.js +10 -0
- package/dist/commands/workflow/instructions.d.ts +39 -0
- package/dist/commands/workflow/instructions.js +565 -0
- package/dist/commands/workflow/new-change.d.ts +19 -0
- package/dist/commands/workflow/new-change.js +135 -0
- package/dist/commands/workflow/shared.d.ts +94 -0
- package/dist/commands/workflow/shared.js +169 -0
- package/dist/commands/workflow/status.d.ts +18 -0
- package/dist/commands/workflow/status.js +201 -0
- package/dist/core/artifact-graph/graph.d.ts +72 -0
- package/dist/core/artifact-graph/graph.js +165 -0
- package/dist/core/artifact-graph/index.d.ts +9 -0
- package/dist/core/artifact-graph/index.js +14 -0
- package/dist/core/artifact-graph/instruction-loader.d.ts +208 -0
- package/dist/core/artifact-graph/instruction-loader.js +268 -0
- package/dist/core/artifact-graph/outputs.d.ts +19 -0
- package/dist/core/artifact-graph/outputs.js +351 -0
- package/dist/core/artifact-graph/resolver.d.ts +89 -0
- package/dist/core/artifact-graph/resolver.js +342 -0
- package/dist/core/artifact-graph/schema.d.ts +27 -0
- package/dist/core/artifact-graph/schema.js +160 -0
- package/dist/core/artifact-graph/state.d.ts +12 -0
- package/dist/core/artifact-graph/state.js +31 -0
- package/dist/core/artifact-graph/types.d.ts +40 -0
- package/dist/core/artifact-graph/types.js +59 -0
- package/dist/core/available-tools.d.ts +18 -0
- package/dist/core/available-tools.js +49 -0
- package/dist/core/change-layout.d.ts +17 -0
- package/dist/core/change-layout.js +30 -0
- package/dist/core/change-metadata/index.d.ts +2 -0
- package/dist/core/change-metadata/index.js +2 -0
- package/dist/core/change-metadata/schema.d.ts +23 -0
- package/dist/core/change-metadata/schema.js +46 -0
- package/dist/core/change-status-policy.d.ts +49 -0
- package/dist/core/change-status-policy.js +50 -0
- package/dist/core/command-generation/adapters/amazon-q.d.ts +17 -0
- package/dist/core/command-generation/adapters/amazon-q.js +32 -0
- package/dist/core/command-generation/adapters/antigravity.d.ts +13 -0
- package/dist/core/command-generation/adapters/antigravity.js +27 -0
- package/dist/core/command-generation/adapters/atomcode.d.ts +24 -0
- package/dist/core/command-generation/adapters/atomcode.js +46 -0
- package/dist/core/command-generation/adapters/auggie.d.ts +13 -0
- package/dist/core/command-generation/adapters/auggie.js +28 -0
- package/dist/core/command-generation/adapters/bob.d.ts +18 -0
- package/dist/core/command-generation/adapters/bob.js +33 -0
- package/dist/core/command-generation/adapters/claude.d.ts +13 -0
- package/dist/core/command-generation/adapters/claude.js +32 -0
- package/dist/core/command-generation/adapters/cline.d.ts +14 -0
- package/dist/core/command-generation/adapters/cline.js +27 -0
- package/dist/core/command-generation/adapters/codeassistant.d.ts +15 -0
- package/dist/core/command-generation/adapters/codeassistant.js +29 -0
- package/dist/core/command-generation/adapters/codebuddy.d.ts +13 -0
- package/dist/core/command-generation/adapters/codebuddy.js +29 -0
- package/dist/core/command-generation/adapters/codestudio.d.ts +13 -0
- package/dist/core/command-generation/adapters/codestudio.js +27 -0
- package/dist/core/command-generation/adapters/command-code.d.ts +19 -0
- package/dist/core/command-generation/adapters/command-code.js +34 -0
- package/dist/core/command-generation/adapters/continue.d.ts +13 -0
- package/dist/core/command-generation/adapters/continue.js +33 -0
- package/dist/core/command-generation/adapters/costrict.d.ts +13 -0
- package/dist/core/command-generation/adapters/costrict.js +28 -0
- package/dist/core/command-generation/adapters/crush.d.ts +13 -0
- package/dist/core/command-generation/adapters/crush.js +30 -0
- package/dist/core/command-generation/adapters/cursor.d.ts +14 -0
- package/dist/core/command-generation/adapters/cursor.js +31 -0
- package/dist/core/command-generation/adapters/devin.d.ts +19 -0
- package/dist/core/command-generation/adapters/devin.js +36 -0
- package/dist/core/command-generation/adapters/easycode.d.ts +17 -0
- package/dist/core/command-generation/adapters/easycode.js +28 -0
- package/dist/core/command-generation/adapters/factory.d.ts +13 -0
- package/dist/core/command-generation/adapters/factory.js +28 -0
- package/dist/core/command-generation/adapters/gemini.d.ts +13 -0
- package/dist/core/command-generation/adapters/gemini.js +27 -0
- package/dist/core/command-generation/adapters/gigacode.d.ts +17 -0
- package/dist/core/command-generation/adapters/gigacode.js +31 -0
- package/dist/core/command-generation/adapters/github-copilot.d.ts +13 -0
- package/dist/core/command-generation/adapters/github-copilot.js +27 -0
- package/dist/core/command-generation/adapters/iflow.d.ts +13 -0
- package/dist/core/command-generation/adapters/iflow.js +30 -0
- package/dist/core/command-generation/adapters/index.d.ts +40 -0
- package/dist/core/command-generation/adapters/index.js +40 -0
- package/dist/core/command-generation/adapters/junie.d.ts +13 -0
- package/dist/core/command-generation/adapters/junie.js +27 -0
- package/dist/core/command-generation/adapters/kilocode.d.ts +14 -0
- package/dist/core/command-generation/adapters/kilocode.js +23 -0
- package/dist/core/command-generation/adapters/kiro.d.ts +13 -0
- package/dist/core/command-generation/adapters/kiro.js +27 -0
- package/dist/core/command-generation/adapters/lingma.d.ts +13 -0
- package/dist/core/command-generation/adapters/lingma.js +30 -0
- package/dist/core/command-generation/adapters/oh-my-pi.d.ts +21 -0
- package/dist/core/command-generation/adapters/oh-my-pi.js +42 -0
- package/dist/core/command-generation/adapters/opencode.d.ts +14 -0
- package/dist/core/command-generation/adapters/opencode.js +38 -0
- package/dist/core/command-generation/adapters/pi.d.ts +18 -0
- package/dist/core/command-generation/adapters/pi.js +39 -0
- package/dist/core/command-generation/adapters/qoder.d.ts +13 -0
- package/dist/core/command-generation/adapters/qoder.js +30 -0
- package/dist/core/command-generation/adapters/qwen.d.ts +17 -0
- package/dist/core/command-generation/adapters/qwen.js +31 -0
- package/dist/core/command-generation/adapters/roocode.d.ts +14 -0
- package/dist/core/command-generation/adapters/roocode.js +27 -0
- package/dist/core/command-generation/adapters/trae.d.ts +13 -0
- package/dist/core/command-generation/adapters/trae.js +28 -0
- package/dist/core/command-generation/adapters/zcode.d.ts +16 -0
- package/dist/core/command-generation/adapters/zcode.js +33 -0
- package/dist/core/command-generation/generator.d.ts +29 -0
- package/dist/core/command-generation/generator.js +43 -0
- package/dist/core/command-generation/index.d.ts +22 -0
- package/dist/core/command-generation/index.js +24 -0
- package/dist/core/command-generation/invocation.d.ts +73 -0
- package/dist/core/command-generation/invocation.js +77 -0
- package/dist/core/command-generation/registry.d.ts +36 -0
- package/dist/core/command-generation/registry.js +114 -0
- package/dist/core/command-generation/toml.d.ts +19 -0
- package/dist/core/command-generation/toml.js +39 -0
- package/dist/core/command-generation/types.d.ts +63 -0
- package/dist/core/command-generation/types.js +8 -0
- package/dist/core/command-generation/yaml.d.ts +36 -0
- package/dist/core/command-generation/yaml.js +48 -0
- package/dist/core/command-surface.d.ts +15 -0
- package/dist/core/command-surface.js +33 -0
- package/dist/core/config-prompts.d.ts +9 -0
- package/dist/core/config-prompts.js +56 -0
- package/dist/core/config-schema.d.ts +98 -0
- package/dist/core/config-schema.js +308 -0
- package/dist/core/config.d.ts +50 -0
- package/dist/core/config.js +122 -0
- package/dist/core/file-state.d.ts +36 -0
- package/dist/core/file-state.js +144 -0
- package/dist/core/file-view.d.ts +13 -0
- package/dist/core/file-view.js +14 -0
- package/dist/core/global-config.d.ts +101 -0
- package/dist/core/global-config.js +182 -0
- package/dist/core/id.d.ts +17 -0
- package/dist/core/id.js +30 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.js +7 -0
- package/dist/core/init.d.ts +57 -0
- package/dist/core/init.js +836 -0
- package/dist/core/list.d.ts +11 -0
- package/dist/core/list.js +197 -0
- package/dist/core/migration.d.ts +121 -0
- package/dist/core/migration.js +540 -0
- package/dist/core/onboarding-commands.d.ts +30 -0
- package/dist/core/onboarding-commands.js +38 -0
- package/dist/core/openspec-root.d.ts +47 -0
- package/dist/core/openspec-root.js +196 -0
- package/dist/core/planning-home.d.ts +16 -0
- package/dist/core/planning-home.js +67 -0
- package/dist/core/profile-sync-drift.d.ts +31 -0
- package/dist/core/profile-sync-drift.js +201 -0
- package/dist/core/profiles.d.ts +19 -0
- package/dist/core/profiles.js +33 -0
- package/dist/core/project-config.d.ts +132 -0
- package/dist/core/project-config.js +519 -0
- package/dist/core/references.d.ts +10 -0
- package/dist/core/references.js +46 -0
- package/dist/core/root-selection.d.ts +59 -0
- package/dist/core/root-selection.js +132 -0
- package/dist/core/shared/allowed-tools.d.ts +12 -0
- package/dist/core/shared/allowed-tools.js +12 -0
- package/dist/core/shared/ide-restart.d.ts +26 -0
- package/dist/core/shared/ide-restart.js +42 -0
- package/dist/core/shared/index.d.ts +11 -0
- package/dist/core/shared/index.js +11 -0
- package/dist/core/shared/skill-content-equivalence.d.ts +7 -0
- package/dist/core/shared/skill-content-equivalence.js +45 -0
- package/dist/core/shared/skill-files.d.ts +7 -0
- package/dist/core/shared/skill-files.js +43 -0
- package/dist/core/shared/skill-generation.d.ts +49 -0
- package/dist/core/shared/skill-generation.js +125 -0
- package/dist/core/shared/skill-paths.d.ts +13 -0
- package/dist/core/shared/skill-paths.js +23 -0
- package/dist/core/shared/tool-detection.d.ts +99 -0
- package/dist/core/shared/tool-detection.js +318 -0
- package/dist/core/shared-skill-target.d.ts +51 -0
- package/dist/core/shared-skill-target.js +239 -0
- package/dist/core/store/errors.d.ts +20 -0
- package/dist/core/store/errors.js +22 -0
- package/dist/core/store/index.d.ts +2 -0
- package/dist/core/store/index.js +2 -0
- package/dist/core/styles/palette.d.ts +7 -0
- package/dist/core/styles/palette.js +8 -0
- package/dist/core/templates/optional-workflow.d.ts +69 -0
- package/dist/core/templates/optional-workflow.js +140 -0
- package/dist/core/templates/types.d.ts +19 -0
- package/dist/core/templates/types.js +5 -0
- package/dist/core/templates/workflows/personal.d.ts +22 -0
- package/dist/core/templates/workflows/personal.js +225 -0
- package/dist/core/update-plan.d.ts +127 -0
- package/dist/core/update-plan.js +390 -0
- package/dist/core/update.d.ts +154 -0
- package/dist/core/update.js +723 -0
- package/dist/core/version-check.d.ts +188 -0
- package/dist/core/version-check.js +906 -0
- package/dist/core/zod-issues.d.ts +4 -0
- package/dist/core/zod-issues.js +10 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/prompts/searchable-multi-select.d.ts +35 -0
- package/dist/prompts/searchable-multi-select.js +188 -0
- package/dist/telemetry/config.d.ts +37 -0
- package/dist/telemetry/config.js +148 -0
- package/dist/telemetry/index.d.ts +39 -0
- package/dist/telemetry/index.js +217 -0
- package/dist/telemetry/opt-out.d.ts +17 -0
- package/dist/telemetry/opt-out.js +27 -0
- package/dist/ui/ascii-patterns.d.ts +16 -0
- package/dist/ui/ascii-patterns.js +133 -0
- package/dist/ui/welcome-screen.d.ts +18 -0
- package/dist/ui/welcome-screen.js +199 -0
- package/dist/utils/change-metadata.d.ts +116 -0
- package/dist/utils/change-metadata.js +324 -0
- package/dist/utils/change-utils.d.ts +79 -0
- package/dist/utils/change-utils.js +121 -0
- package/dist/utils/ci.d.ts +12 -0
- package/dist/utils/ci.js +16 -0
- package/dist/utils/command-references.d.ts +104 -0
- package/dist/utils/command-references.js +190 -0
- package/dist/utils/date.d.ts +8 -0
- package/dist/utils/date.js +13 -0
- package/dist/utils/executable-path.d.ts +5 -0
- package/dist/utils/executable-path.js +44 -0
- package/dist/utils/file-system.d.ts +58 -0
- package/dist/utils/file-system.js +432 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.js +9 -0
- package/dist/utils/interactive.d.ts +85 -0
- package/dist/utils/interactive.js +174 -0
- package/dist/utils/item-discovery.d.ts +15 -0
- package/dist/utils/item-discovery.js +33 -0
- package/dist/utils/line-endings.d.ts +34 -0
- package/dist/utils/line-endings.js +46 -0
- package/dist/utils/match.d.ts +3 -0
- package/dist/utils/match.js +22 -0
- package/dist/utils/nested-change.d.ts +35 -0
- package/dist/utils/nested-change.js +185 -0
- package/dist/utils/shell-detection.d.ts +20 -0
- package/dist/utils/shell-detection.js +94 -0
- package/dist/utils/task-progress.d.ts +67 -0
- package/dist/utils/task-progress.js +184 -0
- package/package.json +74 -4
- package/schemas//351/273/230/350/256/244/schema.yaml +68 -0
- package/schemas//351/273/230/350/256/244/templates//344/273/273/345/212/241/346/270/205/345/215/225.md +25 -0
- package/schemas//351/273/230/350/256/244/templates//346/265/213/350/257/225/347/224/250/344/276/213.md +34 -0
- package/schemas//351/273/230/350/256/244/templates//350/256/276/350/256/241/346/226/207/346/241/243.md +172 -0
- package/schemas//351/273/230/350/256/244/templates//351/234/200/346/261/202/346/226/207/346/241/243.md +19 -0
- package/skill-files//344/273/273/345/212/241/references//346/212/244/346/240/217.md +43 -0
- package/skill-files//344/273/273/345/212/241/references//347/262/222/345/272/246.md +23 -0
- package/skill-files//344/273/273/345/212/241/references//347/272/246/346/235/237.md +8 -0
- package/skill-files//344/273/273/345/212/241/scripts/check-tasks.mjs +83 -0
- package/skill-files//345/206/231Java/references/Java/350/247/204/350/214/203.md +25 -0
- package/skill-files//345/206/231Java/references//345/215/261/351/231/251/345/206/231/346/263/225.md +27 -0
- package/skill-files//345/206/231Java/references//345/256/214/346/210/220/345/257/271/347/205/247.md +15 -0
- package/skill-files//345/206/231Java/references//346/212/244/346/240/217.md +35 -0
- package/skill-files//345/206/231Java/references//347/272/246/346/235/237.md +8 -0
- package/skill-files//345/206/231Java/references//350/270/251/345/235/221.md +17 -0
- package/skill-files//345/256/241Java/references//346/212/244/346/240/217.md +19 -0
- package/skill-files//345/256/241Java/references//347/244/272/344/276/213.md +23 -0
- package/skill-files//345/256/241Java/references//347/272/246/346/235/237.md +7 -0
- package/skill-files//345/256/241Java/references//350/247/204/345/210/231.md +23 -0
- package/skill-files//345/256/241Java/references//351/242/206/345/237/237/346/243/200/351/241/271.md +58 -0
- package/skill-files//345/256/241/346/226/207/346/241/243/references//346/212/244/346/240/217.md +18 -0
- package/skill-files//345/256/241/346/226/207/346/241/243/references//347/272/246/346/235/237.md +8 -0
- package/skill-files//345/256/241/346/226/207/346/241/243/references//347/273/264/345/272/246.md +17 -0
- package/skill-files//345/256/241/346/226/207/346/241/243/references//350/270/251/345/235/221.md +11 -0
- package/skill-files//346/226/260/345/273/272/references//344/272/244/346/216/245.md +11 -0
- package/skill-files//346/226/260/345/273/272/references//346/212/244/346/240/217.md +14 -0
- package/skill-files//346/226/260/345/273/272/references//347/272/246/346/235/237.md +7 -0
- package/skill-files//346/267/261/346/200/235/config/prompts.yaml +114 -0
- package/skill-files//346/267/261/346/200/235/references//344/270/211/346/226/271/346/240/270/345/257/271.md +72 -0
- package/skill-files//346/267/261/346/200/235/references//344/276/213/345/255/220.md +39 -0
- package/skill-files//346/267/261/346/200/235/references//346/200/216/344/271/210/346/203/263.md +61 -0
- package/skill-files//346/267/261/346/200/235/references//346/212/244/346/240/217.md +26 -0
- package/skill-files//346/267/261/346/200/235/references//347/202/271/351/200/211/346/217/220/351/227/256.md +66 -0
- package/skill-files//346/267/261/346/200/235/references//347/231/273/350/256/260.md +45 -0
- package/skill-files//346/267/261/346/200/235/references//347/272/246/346/235/237.md +9 -0
- package/skill-files//347/224/250/344/276/213/references//346/200/216/344/271/210/345/206/231.md +13 -0
- package/skill-files//347/224/250/344/276/213/references//346/212/244/346/240/217.md +42 -0
- package/skill-files//347/224/250/344/276/213/references//347/272/246/346/235/237.md +5 -0
- package/skill-files//347/224/250/344/276/213/scripts/check-cases.mjs +70 -0
- package/skill-files//350/256/276/350/256/241/references//345/207/206/345/244/207.md +25 -0
- package/skill-files//350/256/276/350/256/241/references//346/212/244/346/240/217.md +134 -0
- package/skill-files//350/256/276/350/256/241/references//346/226/207/351/243/216.md +17 -0
- package/skill-files//350/256/276/350/256/241/references//347/225/214/351/235/242.md +18 -0
- package/skill-files//350/256/276/350/256/241/references//347/261/273/345/236/213.md +19 -0
- package/skill-files//350/256/276/350/256/241/references//347/272/246/346/235/237.md +25 -0
- package/skill-files//350/256/276/350/256/241/scripts/_smoke.mjs +72 -0
- package/skill-files//350/256/276/350/256/241/scripts/check-design.mjs +331 -0
- package/skill-files//351/234/200/346/261/202/references//346/212/244/346/240/217.md +26 -0
- package/skill-files//351/234/200/346/261/202/references//346/226/207/351/243/216.md +23 -0
- package/skill-files//351/234/200/346/261/202/references//346/267/261/346/200/235/346/221/204/345/205/245.md +21 -0
- package/skill-files//351/234/200/346/261/202/references//347/272/246/346/235/237.md +8 -0
- package/skill-files//351/234/200/346/261/202/references//351/234/200/346/261/202/345/276/205/345/256/232.md +13 -0
- package/skill-files//351/234/200/346/261/202/scripts/check-requirements.mjs +60 -0
- package/skill-files//351/252/214/346/224/266/references//346/212/244/346/240/217.md +27 -0
- package/skill-files//351/252/214/346/224/266/references//346/240/270/345/257/271.md +17 -0
- package/skill-files//351/252/214/346/224/266/references//347/272/246/346/235/237.md +7 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const OPERATION_IDS: readonly ["apply", "archive"];
|
|
3
|
+
export type OperationId = (typeof OPERATION_IDS)[number];
|
|
4
|
+
export interface OperationConfig {
|
|
5
|
+
guidance?: string[];
|
|
6
|
+
}
|
|
7
|
+
export type OperationsConfig = Partial<Record<OperationId, OperationConfig>>;
|
|
8
|
+
/**
|
|
9
|
+
* Zod schema for project configuration.
|
|
10
|
+
*
|
|
11
|
+
* Purpose:
|
|
12
|
+
* 1. Documentation - clearly defines the config file structure
|
|
13
|
+
* 2. Type safety - TypeScript infers ProjectConfig type from schema
|
|
14
|
+
* 3. Runtime validation - uses safeParse() for resilient field-by-field validation
|
|
15
|
+
*
|
|
16
|
+
* Why Zod over manual validation:
|
|
17
|
+
* - Helps understand FreeSpec's data interfaces at a glance
|
|
18
|
+
* - Single source of truth for type and validation
|
|
19
|
+
* - Consistent with other FreeSpec schemas
|
|
20
|
+
*/
|
|
21
|
+
export declare const ProjectConfigSchema: z.ZodObject<{
|
|
22
|
+
schema: z.ZodString;
|
|
23
|
+
context: z.ZodOptional<z.ZodString>;
|
|
24
|
+
rules: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
|
|
25
|
+
operations: z.ZodOptional<z.ZodObject<{
|
|
26
|
+
apply: z.ZodOptional<z.ZodObject<{
|
|
27
|
+
guidance: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
28
|
+
}, z.core.$strip>>;
|
|
29
|
+
archive: z.ZodOptional<z.ZodObject<{
|
|
30
|
+
guidance: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
31
|
+
}, z.core.$strip>>;
|
|
32
|
+
}, z.core.$strip>>;
|
|
33
|
+
store: z.ZodOptional<z.ZodString>;
|
|
34
|
+
githubCopilot: z.ZodOptional<z.ZodObject<{
|
|
35
|
+
cloudAgent: z.ZodOptional<z.ZodBoolean>;
|
|
36
|
+
}, z.core.$strip>>;
|
|
37
|
+
}, z.core.$strip>;
|
|
38
|
+
/** Normalized in-memory shape of a referenced store declaration. */
|
|
39
|
+
export interface DeclarationEntry {
|
|
40
|
+
id: string;
|
|
41
|
+
/** Clone source rendered into onboarding fixes. */
|
|
42
|
+
remote?: string;
|
|
43
|
+
}
|
|
44
|
+
export type ProjectConfig = z.infer<typeof ProjectConfigSchema> & {
|
|
45
|
+
references?: DeclarationEntry[];
|
|
46
|
+
};
|
|
47
|
+
export interface OperationInputs {
|
|
48
|
+
context?: string;
|
|
49
|
+
operationGuidance?: string[];
|
|
50
|
+
}
|
|
51
|
+
export declare function loadOperationInputs(projectConfig: ProjectConfig | null, operationId: OperationId): OperationInputs;
|
|
52
|
+
export declare const MAX_CONTEXT_SIZE: number;
|
|
53
|
+
/**
|
|
54
|
+
* Read and parse freespec/config.yaml from project root.
|
|
55
|
+
* Uses resilient parsing - validates each field independently using Zod safeParse.
|
|
56
|
+
* Returns null if file doesn't exist.
|
|
57
|
+
* Returns partial config if some fields are invalid (with warnings).
|
|
58
|
+
*
|
|
59
|
+
* Performance note (Jan 2025):
|
|
60
|
+
* Benchmarks showed direct file reads are fast enough without caching:
|
|
61
|
+
* - Typical config (1KB): ~0.5ms per read
|
|
62
|
+
* - Large config (50KB): ~1.6ms per read
|
|
63
|
+
* - Missing config: ~0.01ms per read
|
|
64
|
+
* Config is read 1-2 times per command (schema resolution + instruction loading),
|
|
65
|
+
* adding ~1-3ms total overhead. Caching would add complexity (mtime checks,
|
|
66
|
+
* invalidation logic) for negligible benefit. Direct reads also ensure config
|
|
67
|
+
* changes are reflected immediately without stale cache issues.
|
|
68
|
+
*
|
|
69
|
+
* @param projectRoot - The root directory of the project (where `freespec/` lives)
|
|
70
|
+
* @returns Parsed config or null if file doesn't exist
|
|
71
|
+
*/
|
|
72
|
+
export declare function readProjectConfig(projectRoot: string): ProjectConfig | null;
|
|
73
|
+
/**
|
|
74
|
+
* Validate artifact IDs in rules against the artifacts of every available
|
|
75
|
+
* schema. The `rules:` map is global, but each change can use a different
|
|
76
|
+
* schema, so a key is only unknown when it matches no artifact in ANY schema.
|
|
77
|
+
* Returns warnings for keys that are unknown everywhere.
|
|
78
|
+
*
|
|
79
|
+
* @param rules - The rules object from config
|
|
80
|
+
* @param validArtifactIds - Set of valid artifact IDs across all schemas
|
|
81
|
+
* @returns Array of warning messages for unknown artifact IDs
|
|
82
|
+
*/
|
|
83
|
+
export declare function validateConfigRules(rules: Record<string, string[]>, validArtifactIds: Set<string>): string[];
|
|
84
|
+
/**
|
|
85
|
+
* Suggest valid schema names when user provides invalid schema.
|
|
86
|
+
* Uses fuzzy matching to find similar names.
|
|
87
|
+
*
|
|
88
|
+
* @param invalidSchemaName - The invalid schema name from config
|
|
89
|
+
* @param availableSchemas - List of available schemas with their type (built-in or project-local)
|
|
90
|
+
* @returns Error message with suggestions and available schemas
|
|
91
|
+
*/
|
|
92
|
+
export declare function suggestSchemas(invalidSchemaName: string, availableSchemas: {
|
|
93
|
+
name: string;
|
|
94
|
+
isBuiltIn: boolean;
|
|
95
|
+
}[]): string;
|
|
96
|
+
export interface StorePointerRead {
|
|
97
|
+
/** The declared store id, when present and a string. */
|
|
98
|
+
value?: string;
|
|
99
|
+
/** Set when the pointer cannot be trusted: the config file could not be
|
|
100
|
+
* read as YAML, or the store key is present but not a string. An empty
|
|
101
|
+
* or comments-only config is NOT malformed - it simply has no pointer. */
|
|
102
|
+
malformed?: 'unparseable' | 'non_string';
|
|
103
|
+
/** Absolute path of the config file actually read, or null when none exists. */
|
|
104
|
+
filePath: string | null;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Warning-silent targeted read of the `store:` pointer. Used by root
|
|
108
|
+
* resolution (which must not re-emit the resilient parser's field
|
|
109
|
+
* warnings) and by `freespec init`'s pointer guard. Unlike
|
|
110
|
+
* `readProjectConfig`, a malformed value is REPORTED, not dropped —
|
|
111
|
+
* a dropped pointer would silently flip where work lands.
|
|
112
|
+
*/
|
|
113
|
+
export declare function readStorePointer(projectRoot: string): StorePointerRead;
|
|
114
|
+
/** Shared .yaml/.yml probe used by readProjectConfig and readStorePointer. */
|
|
115
|
+
export declare function resolveConfigFilePath(projectRoot: string): string | null;
|
|
116
|
+
/** Human rendering of a malformed pointer reason, shared by every surface. */
|
|
117
|
+
export declare function storePointerProblem(reason: 'unparseable' | 'non_string'): string;
|
|
118
|
+
export interface FreeSpecDirClassification {
|
|
119
|
+
/** True when freespec/ has a `<名称>变更` directory. */
|
|
120
|
+
hasPlanningShape: boolean;
|
|
121
|
+
pointer: StorePointerRead;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* One classification for "real root vs config-only pointer dir", shared
|
|
125
|
+
* by root resolution and the init pointer guard so they can never
|
|
126
|
+
* disagree (slice 3.2).
|
|
127
|
+
*
|
|
128
|
+
* A bare `freespec/` directory is not a planning root. The qualifying walk
|
|
129
|
+
* also accepts a config file, so a fresh init is still a root.
|
|
130
|
+
*/
|
|
131
|
+
export declare function classifyFreeSpecDir(projectRoot: string): FreeSpecDirClassification;
|
|
132
|
+
//# sourceMappingURL=project-config.d.ts.map
|
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { parse as parseYaml } from 'yaml';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { isChangeDirectoryName } from './change-layout.js';
|
|
6
|
+
export const OPERATION_IDS = ['apply', 'archive'];
|
|
7
|
+
const OperationConfigSchema = z.object({
|
|
8
|
+
guidance: z.array(z.string()).optional(),
|
|
9
|
+
});
|
|
10
|
+
/**
|
|
11
|
+
* Zod schema for project configuration.
|
|
12
|
+
*
|
|
13
|
+
* Purpose:
|
|
14
|
+
* 1. Documentation - clearly defines the config file structure
|
|
15
|
+
* 2. Type safety - TypeScript infers ProjectConfig type from schema
|
|
16
|
+
* 3. Runtime validation - uses safeParse() for resilient field-by-field validation
|
|
17
|
+
*
|
|
18
|
+
* Why Zod over manual validation:
|
|
19
|
+
* - Helps understand FreeSpec's data interfaces at a glance
|
|
20
|
+
* - Single source of truth for type and validation
|
|
21
|
+
* - Consistent with other FreeSpec schemas
|
|
22
|
+
*/
|
|
23
|
+
export const ProjectConfigSchema = z.object({
|
|
24
|
+
// Required: which schema to use (e.g., "spec-driven", or project-local schema name)
|
|
25
|
+
schema: z
|
|
26
|
+
.string()
|
|
27
|
+
.min(1)
|
|
28
|
+
.describe('The workflow schema to use (e.g., "默认")'),
|
|
29
|
+
// Optional: project context (injected into all artifact instructions)
|
|
30
|
+
// Max size: 50KB (enforced during parsing)
|
|
31
|
+
context: z
|
|
32
|
+
.string()
|
|
33
|
+
.optional()
|
|
34
|
+
.describe('Project context injected into all artifact instructions'),
|
|
35
|
+
// Optional: per-artifact rules (additive to schema's built-in guidance)
|
|
36
|
+
rules: z
|
|
37
|
+
.record(z.string(), // artifact ID
|
|
38
|
+
z.array(z.string()) // list of rules
|
|
39
|
+
)
|
|
40
|
+
.optional()
|
|
41
|
+
.describe('Per-artifact rules, keyed by artifact ID'),
|
|
42
|
+
// Optional: per-operation advisory guidance, kept separate from artifact rules.
|
|
43
|
+
operations: z
|
|
44
|
+
.object({
|
|
45
|
+
apply: OperationConfigSchema.optional(),
|
|
46
|
+
archive: OperationConfigSchema.optional(),
|
|
47
|
+
})
|
|
48
|
+
.optional()
|
|
49
|
+
.describe('Per-operation advisory guidance'),
|
|
50
|
+
// Note: the `references` field (id strings or {id, remote} maps) is
|
|
51
|
+
// deliberately absent here — readProjectConfig parses and normalizes
|
|
52
|
+
// it by hand (see DeclarationEntry below); a schema entry nothing
|
|
53
|
+
// parses would only drift from the real behavior.
|
|
54
|
+
// Optional: the declared default store. Only consulted by root
|
|
55
|
+
// resolution when this freespec/ directory is config-only (no specs/
|
|
56
|
+
// directory and no `<名称>变更` folder); a fallback, never an override.
|
|
57
|
+
store: z
|
|
58
|
+
.string()
|
|
59
|
+
.optional()
|
|
60
|
+
.describe('Store id used as the FreeSpec root when no local planning shape exists'),
|
|
61
|
+
// Optional: GitHub Copilot integration preferences. `cloudAgent` is the
|
|
62
|
+
// opt-in for generating the Copilot cloud coding-agent files (a GitHub
|
|
63
|
+
// Actions workflow + agent file); absent means "not yet decided".
|
|
64
|
+
githubCopilot: z
|
|
65
|
+
.object({
|
|
66
|
+
cloudAgent: z.boolean().optional(),
|
|
67
|
+
})
|
|
68
|
+
.optional()
|
|
69
|
+
.describe('GitHub Copilot integration preferences'),
|
|
70
|
+
});
|
|
71
|
+
export function loadOperationInputs(projectConfig, operationId) {
|
|
72
|
+
const context = projectConfig?.context !== undefined && projectConfig.context.trim().length > 0
|
|
73
|
+
? projectConfig.context
|
|
74
|
+
: undefined;
|
|
75
|
+
const guidance = projectConfig?.operations?.[operationId]?.guidance;
|
|
76
|
+
const operationGuidance = guidance && guidance.length > 0 ? guidance : undefined;
|
|
77
|
+
return {
|
|
78
|
+
...(context !== undefined ? { context } : {}),
|
|
79
|
+
...(operationGuidance !== undefined ? { operationGuidance } : {}),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function parseOperations(raw) {
|
|
83
|
+
if (raw === undefined) {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
87
|
+
console.warn(`Invalid 'operations' field in config (must be object)`);
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
const supported = new Set(OPERATION_IDS);
|
|
91
|
+
const operations = {};
|
|
92
|
+
for (const [operationId, value] of Object.entries(raw)) {
|
|
93
|
+
if (!supported.has(operationId)) {
|
|
94
|
+
console.warn(`Unknown operation ID '${operationId}' in config. Supported operation IDs: ${OPERATION_IDS.join(', ')}`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const typedOperationId = operationId;
|
|
98
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
99
|
+
console.warn(`Invalid 'operations.${operationId}' field in config (must be object), ignoring this operation`);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const operation = value;
|
|
103
|
+
const unknownFields = Object.keys(operation).filter((field) => field !== 'guidance');
|
|
104
|
+
if (unknownFields.length > 0) {
|
|
105
|
+
console.warn(`Unknown field(s) in 'operations.${operationId}': ${unknownFields.join(', ')}. Supported fields: guidance`);
|
|
106
|
+
}
|
|
107
|
+
if (operation.guidance === undefined) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const guidanceResult = z.array(z.string()).safeParse(operation.guidance);
|
|
111
|
+
if (!guidanceResult.success) {
|
|
112
|
+
console.warn(`Guidance for operation '${operationId}' must be an array of strings, ignoring this operation's guidance`);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const guidance = guidanceResult.data.filter((entry) => entry.length > 0);
|
|
116
|
+
if (guidance.length < guidanceResult.data.length) {
|
|
117
|
+
console.warn(`Some guidance for operation '${operationId}' are empty strings, ignoring them`);
|
|
118
|
+
}
|
|
119
|
+
if (guidance.length > 0) {
|
|
120
|
+
operations[typedOperationId] = { guidance };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return Object.keys(operations).length > 0 ? operations : undefined;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Parser for `references:` declarations: string entries or
|
|
127
|
+
* {id, remote} maps, normalized to DeclarationEntry[]. Dedup keys on
|
|
128
|
+
* id and keeps the first position; the first entry carrying a remote
|
|
129
|
+
* supplies it (a later duplicate fills a missing remote, never
|
|
130
|
+
* overrides). Invalid entries drop with a warning like other resilient
|
|
131
|
+
* fields; returns undefined when the field is absent or normalizes to
|
|
132
|
+
* empty.
|
|
133
|
+
*/
|
|
134
|
+
function parseDeclarationList(raw) {
|
|
135
|
+
const fieldName = 'references';
|
|
136
|
+
if (raw === undefined) {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
if (!Array.isArray(raw)) {
|
|
140
|
+
console.warn(`Invalid '${fieldName}' field in config (must be an array of store ids)`);
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
const byId = new Map();
|
|
144
|
+
let droppedEntries = false;
|
|
145
|
+
let droppedRemotes = false;
|
|
146
|
+
for (const entry of raw) {
|
|
147
|
+
let declaration = null;
|
|
148
|
+
if (typeof entry === 'string') {
|
|
149
|
+
declaration = { id: entry };
|
|
150
|
+
}
|
|
151
|
+
else if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
|
|
152
|
+
const candidate = entry;
|
|
153
|
+
if (typeof candidate.id === 'string') {
|
|
154
|
+
declaration = { id: candidate.id };
|
|
155
|
+
if (typeof candidate.remote === 'string' && candidate.remote.length > 0) {
|
|
156
|
+
declaration.remote = candidate.remote;
|
|
157
|
+
}
|
|
158
|
+
else if (candidate.remote !== undefined) {
|
|
159
|
+
droppedRemotes = true; // remote dropped, id kept
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (!declaration) {
|
|
164
|
+
droppedEntries = true;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const existing = byId.get(declaration.id);
|
|
168
|
+
if (!existing) {
|
|
169
|
+
byId.set(declaration.id, declaration);
|
|
170
|
+
}
|
|
171
|
+
else if (existing.remote === undefined && declaration.remote !== undefined) {
|
|
172
|
+
existing.remote = declaration.remote;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (droppedEntries) {
|
|
176
|
+
console.warn(`Some '${fieldName}' entries are invalid, ignoring them`);
|
|
177
|
+
}
|
|
178
|
+
if (droppedRemotes) {
|
|
179
|
+
console.warn(`Some '${fieldName}' remotes are not non-empty strings; the ids are kept without a clone source`);
|
|
180
|
+
}
|
|
181
|
+
return byId.size > 0 ? [...byId.values()] : undefined;
|
|
182
|
+
}
|
|
183
|
+
export const MAX_CONTEXT_SIZE = 50 * 1024; // 50KB hard limit, shared with the references index
|
|
184
|
+
/**
|
|
185
|
+
* Build the warning for an artifact whose `rules:` list is not an array of
|
|
186
|
+
* strings. Names the offending index and what YAML actually produced there, so
|
|
187
|
+
* a config that silently loses an entire rule set can be fixed without
|
|
188
|
+
* bisecting the list by hand. A bare `-` item containing an unquoted ": " is the
|
|
189
|
+
* common cause: YAML reads it as a mapping, so the hint points at quoting.
|
|
190
|
+
*/
|
|
191
|
+
function describeRulesShapeError(artifactId, rules) {
|
|
192
|
+
const base = `Rules for '${artifactId}' must be an array of strings, ignoring this artifact's rules`;
|
|
193
|
+
if (!Array.isArray(rules)) {
|
|
194
|
+
return `${base}. rules.${artifactId} is ${describeYamlType(rules)}`;
|
|
195
|
+
}
|
|
196
|
+
const bad = rules
|
|
197
|
+
.map((rule, index) => ({ rule, index }))
|
|
198
|
+
.filter(({ rule }) => typeof rule !== 'string');
|
|
199
|
+
if (bad.length === 0) {
|
|
200
|
+
return base;
|
|
201
|
+
}
|
|
202
|
+
// Name every offending index with its own shape, so a mixed list does not
|
|
203
|
+
// have to be re-bisected one item at a time.
|
|
204
|
+
const details = bad
|
|
205
|
+
.map(({ rule, index }) => `rules.${artifactId}[${index}] is ${describeYamlType(rule)}`)
|
|
206
|
+
.join('; ');
|
|
207
|
+
// A bare `-` item with an unquoted ": " is the common cause: YAML reads it as
|
|
208
|
+
// a mapping. Say so rather than leaving the reader to work out the quoting.
|
|
209
|
+
const hasMapping = bad.some(({ rule }) => rule !== null && typeof rule === 'object' && !Array.isArray(rule));
|
|
210
|
+
const hint = hasMapping
|
|
211
|
+
? ' — an unquoted ": " makes YAML read the item as a key/value pair; quote the whole scalar to keep it a string.'
|
|
212
|
+
: '';
|
|
213
|
+
return `${base}. ${details}${hint}`;
|
|
214
|
+
}
|
|
215
|
+
/** Name a YAML value's shape in a warning, e.g. "a mapping" or "a number". */
|
|
216
|
+
function describeYamlType(value) {
|
|
217
|
+
if (value === null)
|
|
218
|
+
return 'null';
|
|
219
|
+
if (Array.isArray(value))
|
|
220
|
+
return 'a nested list';
|
|
221
|
+
const type = typeof value;
|
|
222
|
+
if (type === 'object')
|
|
223
|
+
return 'a mapping';
|
|
224
|
+
if (type === 'number' || type === 'boolean')
|
|
225
|
+
return `a ${type}`;
|
|
226
|
+
return `a ${type}`;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Read and parse freespec/config.yaml from project root.
|
|
230
|
+
* Uses resilient parsing - validates each field independently using Zod safeParse.
|
|
231
|
+
* Returns null if file doesn't exist.
|
|
232
|
+
* Returns partial config if some fields are invalid (with warnings).
|
|
233
|
+
*
|
|
234
|
+
* Performance note (Jan 2025):
|
|
235
|
+
* Benchmarks showed direct file reads are fast enough without caching:
|
|
236
|
+
* - Typical config (1KB): ~0.5ms per read
|
|
237
|
+
* - Large config (50KB): ~1.6ms per read
|
|
238
|
+
* - Missing config: ~0.01ms per read
|
|
239
|
+
* Config is read 1-2 times per command (schema resolution + instruction loading),
|
|
240
|
+
* adding ~1-3ms total overhead. Caching would add complexity (mtime checks,
|
|
241
|
+
* invalidation logic) for negligible benefit. Direct reads also ensure config
|
|
242
|
+
* changes are reflected immediately without stale cache issues.
|
|
243
|
+
*
|
|
244
|
+
* @param projectRoot - The root directory of the project (where `freespec/` lives)
|
|
245
|
+
* @returns Parsed config or null if file doesn't exist
|
|
246
|
+
*/
|
|
247
|
+
export function readProjectConfig(projectRoot) {
|
|
248
|
+
const configPath = resolveConfigFilePath(projectRoot);
|
|
249
|
+
if (configPath === null) {
|
|
250
|
+
return null; // No config is OK
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
254
|
+
const raw = parseYaml(content);
|
|
255
|
+
if (!raw || typeof raw !== 'object') {
|
|
256
|
+
console.warn(`freespec/config.yaml is not a valid YAML object`);
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
const config = {};
|
|
260
|
+
// Parse schema field using Zod
|
|
261
|
+
const schemaField = z.string().min(1);
|
|
262
|
+
const schemaResult = schemaField.safeParse(raw.schema);
|
|
263
|
+
if (schemaResult.success) {
|
|
264
|
+
config.schema = schemaResult.data;
|
|
265
|
+
}
|
|
266
|
+
else if (raw.schema !== undefined) {
|
|
267
|
+
console.warn(`Invalid 'schema' field in config (must be non-empty string)`);
|
|
268
|
+
}
|
|
269
|
+
// Parse context field with size limit
|
|
270
|
+
if (raw.context !== undefined) {
|
|
271
|
+
const contextField = z.string();
|
|
272
|
+
const contextResult = contextField.safeParse(raw.context);
|
|
273
|
+
if (contextResult.success) {
|
|
274
|
+
const contextSize = Buffer.byteLength(contextResult.data, 'utf-8');
|
|
275
|
+
if (contextSize > MAX_CONTEXT_SIZE) {
|
|
276
|
+
console.warn(`Context too large (${(contextSize / 1024).toFixed(1)}KB, limit: ${MAX_CONTEXT_SIZE / 1024}KB)`);
|
|
277
|
+
console.warn(`Ignoring context field`);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
config.context = contextResult.data;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
console.warn(`Invalid 'context' field in config (must be string)`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// Parse rules field using Zod
|
|
288
|
+
if (raw.rules !== undefined) {
|
|
289
|
+
const rulesField = z.record(z.string(), z.array(z.string()));
|
|
290
|
+
// First check if it's an object structure (guard against null since typeof null === 'object')
|
|
291
|
+
if (typeof raw.rules === 'object' && raw.rules !== null && !Array.isArray(raw.rules)) {
|
|
292
|
+
// Artifact ids are intentionally not restricted to the built-in naming
|
|
293
|
+
// convention, so keys such as "constructor" remain valid for custom
|
|
294
|
+
// schemas. A null-prototype map preserves those keys as data without
|
|
295
|
+
// letting "__proto__" mutate the lookup object's prototype.
|
|
296
|
+
const parsedRules = Object.create(null);
|
|
297
|
+
let hasValidRules = false;
|
|
298
|
+
for (const [artifactId, rules] of Object.entries(raw.rules)) {
|
|
299
|
+
const rulesArrayResult = z.array(z.string()).safeParse(rules);
|
|
300
|
+
if (rulesArrayResult.success) {
|
|
301
|
+
// Filter out empty strings
|
|
302
|
+
const validRules = rulesArrayResult.data.filter((r) => r.length > 0);
|
|
303
|
+
if (validRules.length > 0) {
|
|
304
|
+
parsedRules[artifactId] = validRules;
|
|
305
|
+
hasValidRules = true;
|
|
306
|
+
}
|
|
307
|
+
if (validRules.length < rulesArrayResult.data.length) {
|
|
308
|
+
console.warn(`Some rules for '${artifactId}' are empty strings, ignoring them`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
console.warn(describeRulesShapeError(artifactId, rules));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (hasValidRules) {
|
|
316
|
+
config.rules = parsedRules;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
console.warn(`Invalid 'rules' field in config (must be object)`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const operations = parseOperations(raw.operations);
|
|
324
|
+
if (operations) {
|
|
325
|
+
config.operations = operations;
|
|
326
|
+
}
|
|
327
|
+
const references = parseDeclarationList(raw.references);
|
|
328
|
+
if (references) {
|
|
329
|
+
config.references = references;
|
|
330
|
+
}
|
|
331
|
+
// Parse store pointer field: a string, or dropped with a warning.
|
|
332
|
+
// (Root resolution does NOT use this parse — it uses readStorePointer
|
|
333
|
+
// below, which errors on malformed pointers instead of dropping.)
|
|
334
|
+
if (raw.store !== undefined) {
|
|
335
|
+
if (typeof raw.store === 'string') {
|
|
336
|
+
config.store = raw.store;
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
console.warn(`Warning: ignoring invalid store: field in ${configPathForWarnings(projectRoot)} (must be a single store id string).`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
// Parse githubCopilot preferences (only cloudAgent is recognized today).
|
|
343
|
+
if (raw.githubCopilot !== undefined) {
|
|
344
|
+
if (typeof raw.githubCopilot === 'object' &&
|
|
345
|
+
raw.githubCopilot !== null &&
|
|
346
|
+
!Array.isArray(raw.githubCopilot)) {
|
|
347
|
+
const cloudAgent = raw.githubCopilot.cloudAgent;
|
|
348
|
+
if (typeof cloudAgent === 'boolean') {
|
|
349
|
+
config.githubCopilot = { cloudAgent };
|
|
350
|
+
}
|
|
351
|
+
else if (cloudAgent !== undefined) {
|
|
352
|
+
console.warn(`Invalid 'githubCopilot.cloudAgent' field in config (must be a boolean)`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
console.warn(`Invalid 'githubCopilot' field in config (must be an object)`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
// Return partial config even if some fields failed
|
|
360
|
+
return Object.keys(config).length > 0 ? config : null;
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
console.warn(`Warning: could not parse ${configPathForWarnings(projectRoot)} (${error instanceof Error ? error.message.split('\n')[0] : String(error)}); ignoring it.`);
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
function configPathForWarnings(projectRoot) {
|
|
368
|
+
return resolveConfigFilePath(projectRoot) ?? path.join(projectRoot, 'freespec', 'config.yaml');
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Validate artifact IDs in rules against the artifacts of every available
|
|
372
|
+
* schema. The `rules:` map is global, but each change can use a different
|
|
373
|
+
* schema, so a key is only unknown when it matches no artifact in ANY schema.
|
|
374
|
+
* Returns warnings for keys that are unknown everywhere.
|
|
375
|
+
*
|
|
376
|
+
* @param rules - The rules object from config
|
|
377
|
+
* @param validArtifactIds - Set of valid artifact IDs across all schemas
|
|
378
|
+
* @returns Array of warning messages for unknown artifact IDs
|
|
379
|
+
*/
|
|
380
|
+
export function validateConfigRules(rules, validArtifactIds) {
|
|
381
|
+
const warnings = [];
|
|
382
|
+
for (const artifactId of Object.keys(rules)) {
|
|
383
|
+
if (!validArtifactIds.has(artifactId)) {
|
|
384
|
+
const validIds = Array.from(validArtifactIds).sort().join(', ');
|
|
385
|
+
warnings.push(`Unknown artifact ID in rules: "${artifactId}". ` +
|
|
386
|
+
`It matches no artifact in any available schema. Known artifact IDs: ${validIds}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return warnings;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Suggest valid schema names when user provides invalid schema.
|
|
393
|
+
* Uses fuzzy matching to find similar names.
|
|
394
|
+
*
|
|
395
|
+
* @param invalidSchemaName - The invalid schema name from config
|
|
396
|
+
* @param availableSchemas - List of available schemas with their type (built-in or project-local)
|
|
397
|
+
* @returns Error message with suggestions and available schemas
|
|
398
|
+
*/
|
|
399
|
+
export function suggestSchemas(invalidSchemaName, availableSchemas) {
|
|
400
|
+
// Simple fuzzy match: Levenshtein distance
|
|
401
|
+
function levenshtein(a, b) {
|
|
402
|
+
const matrix = [];
|
|
403
|
+
for (let i = 0; i <= b.length; i++) {
|
|
404
|
+
matrix[i] = [i];
|
|
405
|
+
}
|
|
406
|
+
for (let j = 0; j <= a.length; j++) {
|
|
407
|
+
matrix[0][j] = j;
|
|
408
|
+
}
|
|
409
|
+
for (let i = 1; i <= b.length; i++) {
|
|
410
|
+
for (let j = 1; j <= a.length; j++) {
|
|
411
|
+
if (b.charAt(i - 1) === a.charAt(j - 1)) {
|
|
412
|
+
matrix[i][j] = matrix[i - 1][j - 1];
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return matrix[b.length][a.length];
|
|
420
|
+
}
|
|
421
|
+
// Find closest matches (distance <= 3)
|
|
422
|
+
const suggestions = availableSchemas
|
|
423
|
+
.map((s) => ({ ...s, distance: levenshtein(invalidSchemaName, s.name) }))
|
|
424
|
+
.filter((s) => s.distance <= 3)
|
|
425
|
+
.sort((a, b) => a.distance - b.distance)
|
|
426
|
+
.slice(0, 3);
|
|
427
|
+
const builtIn = availableSchemas.filter((s) => s.isBuiltIn).map((s) => s.name);
|
|
428
|
+
const projectLocal = availableSchemas.filter((s) => !s.isBuiltIn).map((s) => s.name);
|
|
429
|
+
let message = `Schema '${invalidSchemaName}' not found in freespec/config.yaml\n\n`;
|
|
430
|
+
if (suggestions.length > 0) {
|
|
431
|
+
message += `Did you mean one of these?\n`;
|
|
432
|
+
suggestions.forEach((s) => {
|
|
433
|
+
const type = s.isBuiltIn ? 'built-in' : 'project-local';
|
|
434
|
+
message += ` - ${s.name} (${type})\n`;
|
|
435
|
+
});
|
|
436
|
+
message += '\n';
|
|
437
|
+
}
|
|
438
|
+
message += `Available schemas:\n`;
|
|
439
|
+
if (builtIn.length > 0) {
|
|
440
|
+
message += ` Built-in: ${builtIn.join(', ')}\n`;
|
|
441
|
+
}
|
|
442
|
+
if (projectLocal.length > 0) {
|
|
443
|
+
message += ` Project-local: ${projectLocal.join(', ')}\n`;
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
message += ` Project-local: (none found)\n`;
|
|
447
|
+
}
|
|
448
|
+
message += `\nFix: Edit freespec/config.yaml and change 'schema: ${invalidSchemaName}' to a valid schema name`;
|
|
449
|
+
return message;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Warning-silent targeted read of the `store:` pointer. Used by root
|
|
453
|
+
* resolution (which must not re-emit the resilient parser's field
|
|
454
|
+
* warnings) and by `freespec init`'s pointer guard. Unlike
|
|
455
|
+
* `readProjectConfig`, a malformed value is REPORTED, not dropped —
|
|
456
|
+
* a dropped pointer would silently flip where work lands.
|
|
457
|
+
*/
|
|
458
|
+
export function readStorePointer(projectRoot) {
|
|
459
|
+
const configPath = resolveConfigFilePath(projectRoot);
|
|
460
|
+
if (configPath === null) {
|
|
461
|
+
return { filePath: null };
|
|
462
|
+
}
|
|
463
|
+
try {
|
|
464
|
+
const raw = parseYaml(readFileSync(configPath, 'utf-8'));
|
|
465
|
+
// Empty, comments-only, or non-mapping configs carry no pointer;
|
|
466
|
+
// they are imperfect, not malformed (readProjectConfig owns the
|
|
467
|
+
// field warnings for those).
|
|
468
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
469
|
+
return { filePath: configPath };
|
|
470
|
+
}
|
|
471
|
+
const value = raw.store;
|
|
472
|
+
if (value === undefined) {
|
|
473
|
+
return { filePath: configPath };
|
|
474
|
+
}
|
|
475
|
+
if (typeof value === 'string') {
|
|
476
|
+
return { value, filePath: configPath };
|
|
477
|
+
}
|
|
478
|
+
return { malformed: 'non_string', filePath: configPath };
|
|
479
|
+
}
|
|
480
|
+
catch {
|
|
481
|
+
return { malformed: 'unparseable', filePath: configPath };
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
/** Shared .yaml/.yml probe used by readProjectConfig and readStorePointer. */
|
|
485
|
+
export function resolveConfigFilePath(projectRoot) {
|
|
486
|
+
const yamlPath = path.join(projectRoot, 'freespec', 'config.yaml');
|
|
487
|
+
if (existsSync(yamlPath)) {
|
|
488
|
+
return yamlPath;
|
|
489
|
+
}
|
|
490
|
+
const ymlPath = path.join(projectRoot, 'freespec', 'config.yml');
|
|
491
|
+
return existsSync(ymlPath) ? ymlPath : null;
|
|
492
|
+
}
|
|
493
|
+
/** Human rendering of a malformed pointer reason, shared by every surface. */
|
|
494
|
+
export function storePointerProblem(reason) {
|
|
495
|
+
return reason === 'unparseable'
|
|
496
|
+
? 'the config file could not be read as YAML'
|
|
497
|
+
: 'the store key must be a single store id string';
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* One classification for "real root vs config-only pointer dir", shared
|
|
501
|
+
* by root resolution and the init pointer guard so they can never
|
|
502
|
+
* disagree (slice 3.2).
|
|
503
|
+
*
|
|
504
|
+
* A bare `freespec/` directory is not a planning root. The qualifying walk
|
|
505
|
+
* also accepts a config file, so a fresh init is still a root.
|
|
506
|
+
*/
|
|
507
|
+
export function classifyFreeSpecDir(projectRoot) {
|
|
508
|
+
const freespecDir = path.join(projectRoot, 'freespec');
|
|
509
|
+
return { hasPlanningShape: hasDirectChangeFolder(freespecDir), pointer: readStorePointer(projectRoot) };
|
|
510
|
+
}
|
|
511
|
+
function hasDirectChangeFolder(freespecDir) {
|
|
512
|
+
try {
|
|
513
|
+
return readdirSync(freespecDir, { withFileTypes: true }).some((entry) => entry.isDirectory() && isChangeDirectoryName(entry.name));
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
//# sourceMappingURL=project-config.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strings rendered into agent guidance. One line in, one line out: control
|
|
3
|
+
* characters and newlines must never let hostile content forge instruction lines.
|
|
4
|
+
*/
|
|
5
|
+
export declare function sanitizeInline(value: string, maxLength?: number): string;
|
|
6
|
+
/** Neutralize the envelope's own tags in repo-supplied text. */
|
|
7
|
+
export declare function escapeEnvelopeTags(value: string): string;
|
|
8
|
+
/** Escape attribute metacharacters so a quote cannot close the attribute. */
|
|
9
|
+
export declare function escapeEnvelopeAttribute(value: string): string;
|
|
10
|
+
//# sourceMappingURL=references.d.ts.map
|