synorch 0.1.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 (105) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +56 -0
  4. package/dist/application/doctor-service.d.ts +24 -0
  5. package/dist/application/doctor-service.d.ts.map +1 -0
  6. package/dist/application/doctor-service.js +508 -0
  7. package/dist/application/doctor-service.js.map +1 -0
  8. package/dist/application/project-discovery.d.ts +23 -0
  9. package/dist/application/project-discovery.d.ts.map +1 -0
  10. package/dist/application/project-discovery.js +741 -0
  11. package/dist/application/project-discovery.js.map +1 -0
  12. package/dist/application/skill-resolver.d.ts +25 -0
  13. package/dist/application/skill-resolver.d.ts.map +1 -0
  14. package/dist/application/skill-resolver.js +90 -0
  15. package/dist/application/skill-resolver.js.map +1 -0
  16. package/dist/application/structure-service.d.ts +11 -0
  17. package/dist/application/structure-service.d.ts.map +1 -0
  18. package/dist/application/structure-service.js +116 -0
  19. package/dist/application/structure-service.js.map +1 -0
  20. package/dist/cli.d.ts +3 -0
  21. package/dist/cli.d.ts.map +1 -0
  22. package/dist/cli.js +170 -0
  23. package/dist/cli.js.map +1 -0
  24. package/dist/domain/config.d.ts +210 -0
  25. package/dist/domain/config.d.ts.map +1 -0
  26. package/dist/domain/config.js +108 -0
  27. package/dist/domain/config.js.map +1 -0
  28. package/dist/domain/errors.d.ts +5 -0
  29. package/dist/domain/errors.d.ts.map +1 -0
  30. package/dist/domain/errors.js +9 -0
  31. package/dist/domain/errors.js.map +1 -0
  32. package/dist/domain/generation.d.ts +21 -0
  33. package/dist/domain/generation.d.ts.map +1 -0
  34. package/dist/domain/generation.js +2 -0
  35. package/dist/domain/generation.js.map +1 -0
  36. package/dist/domain/product.d.ts +3 -0
  37. package/dist/domain/product.d.ts.map +1 -0
  38. package/dist/domain/product.js +3 -0
  39. package/dist/domain/product.js.map +1 -0
  40. package/dist/domain/skill-packs.d.ts +39 -0
  41. package/dist/domain/skill-packs.d.ts.map +1 -0
  42. package/dist/domain/skill-packs.js +98 -0
  43. package/dist/domain/skill-packs.js.map +1 -0
  44. package/dist/domain/skill-sources.d.ts +20 -0
  45. package/dist/domain/skill-sources.d.ts.map +1 -0
  46. package/dist/domain/skill-sources.js +95 -0
  47. package/dist/domain/skill-sources.js.map +1 -0
  48. package/dist/infrastructure/bundled-skill-library.d.ts +10 -0
  49. package/dist/infrastructure/bundled-skill-library.d.ts.map +1 -0
  50. package/dist/infrastructure/bundled-skill-library.js +108 -0
  51. package/dist/infrastructure/bundled-skill-library.js.map +1 -0
  52. package/dist/infrastructure/file-system.d.ts +21 -0
  53. package/dist/infrastructure/file-system.d.ts.map +1 -0
  54. package/dist/infrastructure/file-system.js +72 -0
  55. package/dist/infrastructure/file-system.js.map +1 -0
  56. package/dist/infrastructure/serialization.d.ts +3 -0
  57. package/dist/infrastructure/serialization.d.ts.map +1 -0
  58. package/dist/infrastructure/serialization.js +11 -0
  59. package/dist/infrastructure/serialization.js.map +1 -0
  60. package/dist/templates/structure-templates.d.ts +4 -0
  61. package/dist/templates/structure-templates.d.ts.map +1 -0
  62. package/dist/templates/structure-templates.js +471 -0
  63. package/dist/templates/structure-templates.js.map +1 -0
  64. package/dist/templates/technology-skill-templates.d.ts +3 -0
  65. package/dist/templates/technology-skill-templates.d.ts.map +1 -0
  66. package/dist/templates/technology-skill-templates.js +73 -0
  67. package/dist/templates/technology-skill-templates.js.map +1 -0
  68. package/package.json +59 -0
  69. package/skill-sources/ingenium/NOTICE.md +11 -0
  70. package/skill-sources/ingenium/skills/db-schema-craft/SKILL.md +127 -0
  71. package/skill-sources/ingenium/skills/debug-detective/SKILL.md +67 -0
  72. package/skill-sources/ingenium/skills/design-system/SKILL.md +57 -0
  73. package/skill-sources/ingenium/skills/docs-sync/SKILL.md +68 -0
  74. package/skill-sources/ingenium/skills/dotnet-backend/SKILL.md +110 -0
  75. package/skill-sources/ingenium/skills/frontend-craft/SKILL.md +69 -0
  76. package/skill-sources/ingenium/skills/game-audio/SKILL.md +73 -0
  77. package/skill-sources/ingenium/skills/game-design/SKILL.md +92 -0
  78. package/skill-sources/ingenium/skills/godot-dev/SKILL.md +78 -0
  79. package/skill-sources/ingenium/skills/human-made-design/SKILL.md +73 -0
  80. package/skill-sources/ingenium/skills/java-backend/SKILL.md +98 -0
  81. package/skill-sources/ingenium/skills/jev/SKILL.md +150 -0
  82. package/skill-sources/ingenium/skills/motion-craft/SKILL.md +66 -0
  83. package/skill-sources/ingenium/skills/multiplayer-netcode/SKILL.md +63 -0
  84. package/skill-sources/ingenium/skills/node-backend/SKILL.md +113 -0
  85. package/skill-sources/ingenium/skills/node-backend/reference.md +144 -0
  86. package/skill-sources/ingenium/skills/perf-audit/SKILL.md +70 -0
  87. package/skill-sources/ingenium/skills/pixel-art-assets/SKILL.md +91 -0
  88. package/skill-sources/ingenium/skills/pixel-art-assets/scripts/px.py +169 -0
  89. package/skill-sources/ingenium/skills/pixel-game-dev/SKILL.md +79 -0
  90. package/skill-sources/ingenium/skills/project-onboard/SKILL.md +78 -0
  91. package/skill-sources/ingenium/skills/pwa-offline/SKILL.md +70 -0
  92. package/skill-sources/ingenium/skills/query-tuning/SKILL.md +137 -0
  93. package/skill-sources/ingenium/skills/react-modern/SKILL.md +92 -0
  94. package/skill-sources/ingenium/skills/refactor-safe/SKILL.md +63 -0
  95. package/skill-sources/ingenium/skills/release-prep/SKILL.md +57 -0
  96. package/skill-sources/ingenium/skills/safe-merge/SKILL.md +91 -0
  97. package/skill-sources/ingenium/skills/session-recap/SKILL.md +95 -0
  98. package/skill-sources/ingenium/skills/session-recap/scripts/extract_session.py +409 -0
  99. package/skill-sources/ingenium/skills/shader-vfx/SKILL.md +70 -0
  100. package/skill-sources/ingenium/skills/tailwind-v4-tokens/SKILL.md +165 -0
  101. package/skill-sources/ingenium/skills/task-conductor/SKILL.md +158 -0
  102. package/skill-sources/ingenium/skills/tauri-game-dev/SKILL.md +81 -0
  103. package/skill-sources/ingenium/skills/ui-ux-design/SKILL.md +96 -0
  104. package/skill-sources/ingenium/skills/vue-modern/SKILL.md +88 -0
  105. package/skill-sources/ingenium/skills/web-kickoff/SKILL.md +63 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-09-20
11
+
12
+ ### Added
13
+
14
+ - Provider-neutral orchestration structure generation for Codex and Claude Code.
15
+ - Safe `inspect`, `init`, `sync`, and `doctor` CLI commands exposed through `syn` and `synorch`.
16
+ - Evidence-driven repository and workspace discovery for JavaScript, TypeScript, Java, Python, Rust, and Go projects.
17
+ - Automatic technology skill selection for React, Java, Spring Boot, JPA, Node.js, Vue, Nuxt, and Tailwind CSS.
18
+ - A bundled, provenance-tracked Ingenium skill catalog with on-demand loading rules.
19
+ - Model profiles, orchestration protocols, minimal context packets, verification contracts, and provider adapters.
20
+
21
+ [Unreleased]: https://github.com/TunahanTuna/synorch/compare/v0.1.0...HEAD
22
+ [0.1.0]: https://github.com/TunahanTuna/synorch/releases/tag/v0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tunahan Tuna
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # Synorch
2
+
3
+ `synorch`, Codex ve Claude Code için provider-bağımsız, orchestrator merkezli agent/skill/protokol yapısı kuran Node.js tabanlı bir CLI'dır. Paket adı `synorch`, kurulumdan sonraki kısa terminal komutu `syn`'dir.
4
+
5
+ ## Kullanım
6
+
7
+ Kurulum yapmadan çalıştırma:
8
+
9
+ ```bash
10
+ npx synorch init
11
+ pnpm dlx synorch init
12
+ yarn dlx synorch init
13
+ ```
14
+
15
+ Global kurulumdan sonra kısa komut:
16
+
17
+ ```bash
18
+ npm install --global synorch
19
+ syn init
20
+ ```
21
+
22
+ ## Geliştirme
23
+
24
+ ```bash
25
+ pnpm install
26
+ pnpm check
27
+ pnpm dev -- inspect
28
+ ```
29
+
30
+ ## Komutlar
31
+
32
+ ```bash
33
+ syn inspect [--target <path>] [--scope workspace|repository]
34
+ syn init [--target <path>] [--scope workspace|repository] [--force]
35
+ syn sync [--target <path>] [--force] [--json]
36
+ syn doctor [--target <path>] [--json]
37
+ ```
38
+
39
+ ## Skill havuzu
40
+
41
+ `sync`, proje sahibinin 33 skill'lik Ingenium havuzunu destek dosyalarıyla birlikte `.ai/skills/library/ingenium/` altına materialize eder ve kaynak/lisans/provenance bilgisini `.ai/skills/catalog.yaml` içinde tutar. React, Java, Node, Vue, JPA ve Tailwind skill'leri yalnız verified stack kanıtıyla otomatik aktive edilir; diğer skill'ler açıklamaları eşleştiğinde on-demand yüklenir. Bütün havuzun aynı anda context'e alınması yasaktır.
42
+
43
+ Anthropic Agent Skills, Superpowers ve Microsoft Agent Skills kaynakları sabit commit kimlikleriyle kataloglanır ancak güvenlik ve lisans incelemesi yapılmadan içerikleri otomatik import veya execute edilmez.
44
+
45
+ `init`, kaynak koda dokunmadan canonical orchestration çekirdeğini kurar. Ardından `sync`, repository veya workspace modüllerini kanıta dayalı olarak keşfeder, proje/skill registry'lerini ve seçilen teknoloji skill'lerini üretir; kullanıcı tarafından değiştirilmiş generated teknoloji skill'lerini yalnızca açık `--force` ile yeniler. Son olarak `doctor`, referans zincirini, canonical base skill kümesini ve path/symlink sınırlarını doğrular.
46
+
47
+ Mimari kararlar için [mimari planı](./docs/AI-ORCHESTRATION-ARCHITECTURE.md) inceleyin.
48
+
49
+ ## Gereksinimler ve proje bilgileri
50
+
51
+ - Node.js 24 veya daha yeni bir sürüm gerekir.
52
+ - Sürüm geçmişi için [CHANGELOG.md](./CHANGELOG.md) dosyasını inceleyin.
53
+ - Güvenlik açıklarını herkese açık issue yerine [güvenlik politikasındaki](./SECURITY.md) özel bildirim akışıyla paylaşın.
54
+ - Synorch, [MIT lisansı](./LICENSE) altında yayımlanır.
55
+
56
+ İlk sürüm notları [Synorch v0.1.0](./docs/releases/v0.1.0.md) belgesinde yer alır.
@@ -0,0 +1,24 @@
1
+ import type { FileSystem } from "../infrastructure/file-system.ts";
2
+ export type DiagnosticSeverity = "error" | "warning" | "info";
3
+ export interface Diagnostic {
4
+ readonly severity: DiagnosticSeverity;
5
+ readonly code: string;
6
+ readonly message: string;
7
+ readonly path?: string;
8
+ }
9
+ export declare class DoctorService {
10
+ private readonly fileSystem;
11
+ constructor(fileSystem: FileSystem);
12
+ diagnose(targetDirectory: string): Promise<readonly Diagnostic[]>;
13
+ private validateWorkspaceChain;
14
+ private validateProjectRecordPaths;
15
+ private validateProjectArtifactPath;
16
+ private validateCommandPaths;
17
+ private validateSkillRegistry;
18
+ private validateRegisteredSkills;
19
+ private isExistingDirectory;
20
+ private isExistingRegularFile;
21
+ private isCanonicalPathWithin;
22
+ private validateYamlFile;
23
+ }
24
+ //# sourceMappingURL=doctor-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor-service.d.ts","sourceRoot":"","sources":["../../src/application/doctor-service.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kCAAkC,CAAC;AAGnE,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;AAE9D,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,QAAQ,EAAE,kBAAkB,CAAC;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAWD,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAa;gBAErB,UAAU,EAAE,UAAU;IAI5B,QAAQ,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,UAAU,EAAE,CAAC;YAyFhE,sBAAsB;YAkItB,0BAA0B;YA+F1B,2BAA2B;YA+B3B,oBAAoB;YAqDpB,qBAAqB;YA4DrB,wBAAwB;YAiHxB,mBAAmB;YAOnB,qBAAqB;YAOrB,qBAAqB;YASrB,gBAAgB;CAgD/B"}
@@ -0,0 +1,508 @@
1
+ import path from "node:path";
2
+ import { manifestSchema, modelProfileSchema, projectRecordSchema, skillRegistrySchema, workspaceSchema, } from "../domain/config.js";
3
+ import { BASE_SKILLS } from "../domain/skill-packs.js";
4
+ import { parseYaml } from "../infrastructure/serialization.js";
5
+ const REQUIRED_PATHS = [
6
+ "AGENTS.md",
7
+ "CLAUDE.md",
8
+ ".ai/constitution.md",
9
+ ".ai/protocols/registry.yaml",
10
+ ".ai/schemas/context-packet.schema.json",
11
+ ".ai/schemas/completion-packet.schema.json",
12
+ ];
13
+ export class DoctorService {
14
+ fileSystem;
15
+ constructor(fileSystem) {
16
+ this.fileSystem = fileSystem;
17
+ }
18
+ async diagnose(targetDirectory) {
19
+ const root = path.resolve(targetDirectory);
20
+ const diagnostics = [];
21
+ for (const requiredPath of REQUIRED_PATHS) {
22
+ if (!(await this.fileSystem.exists(path.join(root, requiredPath)))) {
23
+ diagnostics.push({
24
+ severity: "error",
25
+ code: "missing-required-file",
26
+ message: `Required structure file is missing: ${requiredPath}`,
27
+ path: requiredPath,
28
+ });
29
+ }
30
+ }
31
+ const manifest = await this.validateYamlFile(root, ".ai/manifest.yaml", manifestSchema, diagnostics);
32
+ const workspace = await this.validateYamlFile(root, ".ai/workspace.yaml", workspaceSchema, diagnostics);
33
+ const openAi = await this.validateYamlFile(root, ".ai/model-profiles/openai.yaml", modelProfileSchema, diagnostics);
34
+ const claude = await this.validateYamlFile(root, ".ai/model-profiles/claude.yaml", modelProfileSchema, diagnostics);
35
+ if (manifest?.routing.require_session_confirmation !== true) {
36
+ diagnostics.push({
37
+ severity: "error",
38
+ code: "session-confirmation-disabled",
39
+ message: "Session model-profile confirmation must remain enabled.",
40
+ path: ".ai/manifest.yaml",
41
+ });
42
+ }
43
+ if (manifest?.routing.silent_fallback !== false) {
44
+ diagnostics.push({
45
+ severity: "error",
46
+ code: "silent-fallback-enabled",
47
+ message: "Silent model fallback is forbidden.",
48
+ path: ".ai/manifest.yaml",
49
+ });
50
+ }
51
+ if (openAi && openAi.provider !== "openai") {
52
+ diagnostics.push({
53
+ severity: "error",
54
+ code: "provider-profile-mismatch",
55
+ message: "OpenAI profile declares a different provider.",
56
+ path: ".ai/model-profiles/openai.yaml",
57
+ });
58
+ }
59
+ if (claude && claude.provider !== "claude") {
60
+ diagnostics.push({
61
+ severity: "error",
62
+ code: "provider-profile-mismatch",
63
+ message: "Claude profile declares a different provider.",
64
+ path: ".ai/model-profiles/claude.yaml",
65
+ });
66
+ }
67
+ if (workspace !== undefined) {
68
+ await this.validateWorkspaceChain(root, workspace, diagnostics);
69
+ }
70
+ if (diagnostics.length === 0) {
71
+ diagnostics.push({
72
+ severity: "info",
73
+ code: "healthy",
74
+ message: "The AI orchestration structure is internally consistent.",
75
+ });
76
+ }
77
+ return diagnostics;
78
+ }
79
+ async validateWorkspaceChain(root, workspace, diagnostics) {
80
+ reportDuplicates(workspace.projects.map((project) => project.id), "project id", "duplicate-project-id", diagnostics);
81
+ reportDuplicates(workspace.projects.map((project) => normalizeForComparison(project.path)), "project path", "duplicate-project-path", diagnostics);
82
+ reportDuplicates(workspace.projects.map((project) => normalizeForComparison(project.record)), "project record", "duplicate-project-record", diagnostics);
83
+ for (const project of workspace.projects) {
84
+ const projectPath = resolveSafeRelativePath(root, project.path);
85
+ if (projectPath === undefined) {
86
+ diagnostics.push({
87
+ severity: "error",
88
+ code: "unsafe-project-path",
89
+ message: `Workspace project path must stay within the target root: ${project.path}`,
90
+ path: ".ai/workspace.yaml",
91
+ });
92
+ continue;
93
+ }
94
+ if (!(await this.isExistingDirectory(projectPath.absolute))) {
95
+ diagnostics.push({
96
+ severity: "error",
97
+ code: "missing-project-directory",
98
+ message: `Workspace project path is not an existing directory: ${project.path}`,
99
+ path: ".ai/workspace.yaml",
100
+ });
101
+ continue;
102
+ }
103
+ if (!(await this.isCanonicalPathWithin(root, projectPath.absolute))) {
104
+ diagnostics.push({
105
+ severity: "error",
106
+ code: "unsafe-project-path",
107
+ message: `Workspace project path escapes the target root: ${project.path}`,
108
+ path: ".ai/workspace.yaml",
109
+ });
110
+ continue;
111
+ }
112
+ const recordPath = resolveSafeRelativePath(root, project.record);
113
+ if (recordPath === undefined) {
114
+ diagnostics.push({
115
+ severity: "error",
116
+ code: "unsafe-project-record-path",
117
+ message: `Workspace project record path must stay within the target root: ${project.record}`,
118
+ path: ".ai/workspace.yaml",
119
+ });
120
+ continue;
121
+ }
122
+ const expectedRecordPath = `.ai/projects/${project.id}.yaml`;
123
+ if (project.record !== expectedRecordPath) {
124
+ diagnostics.push({
125
+ severity: "error",
126
+ code: "noncanonical-project-record-path",
127
+ message: `Workspace project record must be exactly '${expectedRecordPath}', received ` +
128
+ `'${project.record}'.`,
129
+ path: ".ai/workspace.yaml",
130
+ });
131
+ continue;
132
+ }
133
+ if (!(await this.isExistingRegularFile(recordPath.absolute)) ||
134
+ !(await this.isCanonicalPathWithin(root, recordPath.absolute))) {
135
+ diagnostics.push({
136
+ severity: "error",
137
+ code: "missing-project-record",
138
+ message: `Project record is missing, unsafe, or not a regular file: ${project.record}`,
139
+ path: recordPath.relative,
140
+ });
141
+ continue;
142
+ }
143
+ const record = await this.validateYamlFile(root, recordPath.relative, projectRecordSchema, diagnostics, {
144
+ missing: "missing-project-record",
145
+ invalid: "invalid-project-record",
146
+ malformed: "malformed-project-record",
147
+ });
148
+ if (record === undefined)
149
+ continue;
150
+ if (record.id !== project.id) {
151
+ diagnostics.push({
152
+ severity: "error",
153
+ code: "project-record-id-mismatch",
154
+ message: `Project record id '${record.id}' does not match workspace id '${project.id}'.`,
155
+ path: recordPath.relative,
156
+ });
157
+ }
158
+ if (normalizeForComparison(record.path) !== projectPath.comparison) {
159
+ diagnostics.push({
160
+ severity: "error",
161
+ code: "project-record-path-mismatch",
162
+ message: `Project record path '${record.path}' does not match workspace path '${project.path}'.`,
163
+ path: recordPath.relative,
164
+ });
165
+ }
166
+ await this.validateProjectRecordPaths(root, projectPath.absolute, record, recordPath.relative, diagnostics);
167
+ await this.validateSkillRegistry(root, record, recordPath.relative, diagnostics);
168
+ }
169
+ }
170
+ async validateProjectRecordPaths(root, projectRoot, record, recordPath, diagnostics) {
171
+ const modulePaths = new Set();
172
+ for (const module of record.modules) {
173
+ const modulePath = resolveSafeRelativePath(root, module.path);
174
+ if (modulePath === undefined ||
175
+ !isPathWithin(projectRoot, modulePath.absolute) ||
176
+ !(await this.isCanonicalPathWithin(projectRoot, modulePath.absolute))) {
177
+ diagnostics.push({
178
+ severity: "error",
179
+ code: "unsafe-module-path",
180
+ message: `Module path must stay within its project root: ${module.path}`,
181
+ path: recordPath,
182
+ });
183
+ continue;
184
+ }
185
+ modulePaths.add(modulePath.comparison);
186
+ if (!(await this.isExistingDirectory(modulePath.absolute))) {
187
+ diagnostics.push({
188
+ severity: "error",
189
+ code: "missing-module-directory",
190
+ message: `Module path is not an existing directory: ${module.path}`,
191
+ path: recordPath,
192
+ });
193
+ }
194
+ for (const manifestPath of module.manifests) {
195
+ await this.validateProjectArtifactPath(root, projectRoot, manifestPath, recordPath, diagnostics);
196
+ }
197
+ for (const evidence of module.evidence) {
198
+ await this.validateProjectArtifactPath(root, projectRoot, evidence.source, recordPath, diagnostics);
199
+ }
200
+ for (const command of Object.values(module.commands)) {
201
+ await this.validateCommandPaths(root, projectRoot, command.cwd, command.source, modulePath.comparison, recordPath, diagnostics);
202
+ }
203
+ }
204
+ for (const manifestPath of record.manifests) {
205
+ await this.validateProjectArtifactPath(root, projectRoot, manifestPath, recordPath, diagnostics);
206
+ }
207
+ for (const evidence of record.evidence) {
208
+ await this.validateProjectArtifactPath(root, projectRoot, evidence.source, recordPath, diagnostics);
209
+ }
210
+ for (const command of Object.values(record.commands)) {
211
+ await this.validateCommandPaths(root, projectRoot, command.cwd, command.source, modulePaths, recordPath, diagnostics);
212
+ }
213
+ }
214
+ async validateProjectArtifactPath(root, projectRoot, candidate, recordPath, diagnostics) {
215
+ const artifactPath = resolveSafeRelativePath(root, candidate);
216
+ if (artifactPath === undefined ||
217
+ !isPathWithin(projectRoot, artifactPath.absolute) ||
218
+ !(await this.isCanonicalPathWithin(projectRoot, artifactPath.absolute))) {
219
+ diagnostics.push({
220
+ severity: "error",
221
+ code: "unsafe-project-artifact-path",
222
+ message: `Manifest or source path must stay within its project root: ${candidate}`,
223
+ path: recordPath,
224
+ });
225
+ return;
226
+ }
227
+ if (!(await this.isExistingRegularFile(artifactPath.absolute))) {
228
+ diagnostics.push({
229
+ severity: "error",
230
+ code: "missing-project-artifact",
231
+ message: `Manifest or source path is not an existing regular file: ${candidate}`,
232
+ path: recordPath,
233
+ });
234
+ }
235
+ }
236
+ async validateCommandPaths(root, projectRoot, cwd, source, expectedModulePath, recordPath, diagnostics) {
237
+ const cwdPath = resolveSafeRelativePath(root, cwd);
238
+ if (cwdPath === undefined ||
239
+ !isPathWithin(projectRoot, cwdPath.absolute) ||
240
+ !(await this.isCanonicalPathWithin(projectRoot, cwdPath.absolute))) {
241
+ diagnostics.push({
242
+ severity: "error",
243
+ code: "unsafe-command-cwd",
244
+ message: `Command cwd must stay within its project root: ${cwd}`,
245
+ path: recordPath,
246
+ });
247
+ }
248
+ else {
249
+ if (!(await this.isExistingDirectory(cwdPath.absolute))) {
250
+ diagnostics.push({
251
+ severity: "error",
252
+ code: "missing-command-cwd",
253
+ message: `Command cwd is not an existing directory: ${cwd}`,
254
+ path: recordPath,
255
+ });
256
+ }
257
+ const matchesModule = typeof expectedModulePath === "string"
258
+ ? cwdPath.comparison === expectedModulePath
259
+ : expectedModulePath.has(cwdPath.comparison);
260
+ if (!matchesModule) {
261
+ diagnostics.push({
262
+ severity: "error",
263
+ code: "command-cwd-module-mismatch",
264
+ message: `Command cwd does not match a discovered module path: ${cwd}`,
265
+ path: recordPath,
266
+ });
267
+ }
268
+ }
269
+ await this.validateProjectArtifactPath(root, projectRoot, source, recordPath, diagnostics);
270
+ }
271
+ async validateSkillRegistry(root, record, recordPath, diagnostics) {
272
+ const registryPath = resolveSafeRelativePath(root, record.skill_registry, ".ai/projects/");
273
+ if (registryPath === undefined) {
274
+ diagnostics.push({
275
+ severity: "error",
276
+ code: "unsafe-skill-registry-path",
277
+ message: "Project skill_registry must be a safe relative path under .ai/projects/: " +
278
+ record.skill_registry,
279
+ path: recordPath,
280
+ });
281
+ return;
282
+ }
283
+ if (!(await this.isExistingRegularFile(registryPath.absolute)) ||
284
+ !(await this.isCanonicalPathWithin(root, registryPath.absolute))) {
285
+ diagnostics.push({
286
+ severity: "error",
287
+ code: "missing-skill-registry",
288
+ message: "Project skill_registry is missing, unsafe, or not a regular file: " +
289
+ record.skill_registry,
290
+ path: registryPath.relative,
291
+ });
292
+ return;
293
+ }
294
+ const registry = await this.validateYamlFile(root, registryPath.relative, skillRegistrySchema, diagnostics, {
295
+ missing: "missing-skill-registry",
296
+ invalid: "invalid-skill-registry",
297
+ malformed: "malformed-skill-registry",
298
+ });
299
+ if (registry === undefined)
300
+ return;
301
+ if (registry.project_id !== record.id) {
302
+ diagnostics.push({
303
+ severity: "error",
304
+ code: "skill-registry-project-mismatch",
305
+ message: `Skill registry project_id '${registry.project_id}' does not match record id ` +
306
+ `'${record.id}'.`,
307
+ path: registryPath.relative,
308
+ });
309
+ }
310
+ await this.validateRegisteredSkills(root, registry, registryPath.relative, diagnostics);
311
+ }
312
+ async validateRegisteredSkills(root, registry, registryPath, diagnostics) {
313
+ const expectedBaseSkills = new Map(BASE_SKILLS.map((skill) => [skill.id, skill.relativePath]));
314
+ reportDuplicates(registry.base_skills.map((skill) => skill.id), "base skill id", "duplicate-base-skill-id", diagnostics, registryPath);
315
+ const registeredBaseSkillIds = new Set(registry.base_skills.map((skill) => skill.id));
316
+ for (const expectedSkill of BASE_SKILLS) {
317
+ if (!registeredBaseSkillIds.has(expectedSkill.id)) {
318
+ diagnostics.push({
319
+ severity: "error",
320
+ code: "missing-base-skill-entry",
321
+ message: `Skill registry is missing canonical base skill '${expectedSkill.id}'.`,
322
+ path: registryPath,
323
+ });
324
+ }
325
+ }
326
+ for (const registeredSkill of registry.base_skills) {
327
+ if (!expectedBaseSkills.has(registeredSkill.id)) {
328
+ diagnostics.push({
329
+ severity: "error",
330
+ code: "unexpected-base-skill-entry",
331
+ message: `Skill registry contains unexpected base skill '${registeredSkill.id}'.`,
332
+ path: registryPath,
333
+ });
334
+ }
335
+ }
336
+ reportDuplicates([
337
+ ...registry.base_skills.map((skill) => normalizeForComparison(skill.relative_path)),
338
+ ...registry.technology_skills.map((skill) => normalizeForComparison(skill.relative_path)),
339
+ ], "registry skill path", "duplicate-registry-skill-path", diagnostics, registryPath);
340
+ for (const skill of registry.base_skills) {
341
+ const expectedPath = expectedBaseSkills.get(skill.id);
342
+ if (expectedPath === undefined)
343
+ continue;
344
+ const skillPath = resolveSafeRelativePath(root, skill.relative_path, ".ai/skills/");
345
+ if (skill.relative_path !== expectedPath ||
346
+ skillPath === undefined ||
347
+ normalizeForComparison(skill.relative_path).startsWith(".ai/skills/technology/")) {
348
+ diagnostics.push({
349
+ severity: "error",
350
+ code: "unsafe-base-skill-path",
351
+ message: `Base skill '${skill.id}' must use canonical path '${expectedPath}', received ` +
352
+ `'${skill.relative_path}'.`,
353
+ path: registryPath,
354
+ });
355
+ continue;
356
+ }
357
+ if (!(await this.isExistingRegularFile(skillPath.absolute)) ||
358
+ !(await this.isCanonicalPathWithin(root, skillPath.absolute))) {
359
+ diagnostics.push({
360
+ severity: "error",
361
+ code: "missing-base-skill",
362
+ message: `Registered base skill is missing or not a regular file: ${skillPath.relative}`,
363
+ path: skillPath.relative,
364
+ });
365
+ }
366
+ }
367
+ for (const skill of registry.technology_skills) {
368
+ const skillPath = resolveSafeRelativePath(root, skill.relative_path, ".ai/skills/technology/") ??
369
+ resolveSafeRelativePath(root, skill.relative_path, ".ai/skills/library/");
370
+ if (skillPath === undefined) {
371
+ diagnostics.push({
372
+ severity: "error",
373
+ code: "unsafe-technology-skill-path",
374
+ message: "Technology skill path must stay under .ai/skills/technology/ or .ai/skills/library/: " +
375
+ skill.relative_path,
376
+ path: registryPath,
377
+ });
378
+ continue;
379
+ }
380
+ if (!(await this.fileSystem.exists(skillPath.absolute)) ||
381
+ (await this.fileSystem.isDirectory(skillPath.absolute)) ||
382
+ !(await this.isCanonicalPathWithin(root, skillPath.absolute))) {
383
+ diagnostics.push({
384
+ severity: "error",
385
+ code: "missing-technology-skill",
386
+ message: `Registered technology skill is missing: ${skillPath.relative}`,
387
+ path: skillPath.relative,
388
+ });
389
+ }
390
+ }
391
+ }
392
+ async isExistingDirectory(targetPath) {
393
+ return ((await this.fileSystem.exists(targetPath)) &&
394
+ (await this.fileSystem.isDirectory(targetPath)));
395
+ }
396
+ async isExistingRegularFile(targetPath) {
397
+ return ((await this.fileSystem.exists(targetPath)) &&
398
+ !(await this.fileSystem.isDirectory(targetPath)));
399
+ }
400
+ async isCanonicalPathWithin(root, targetPath) {
401
+ try {
402
+ await this.fileSystem.assertPathWithinRoot(root, targetPath);
403
+ return true;
404
+ }
405
+ catch {
406
+ return false;
407
+ }
408
+ }
409
+ async validateYamlFile(root, relativePath, schema, diagnostics, codes = {
410
+ missing: "missing-config",
411
+ invalid: "invalid-config",
412
+ malformed: "invalid-yaml",
413
+ }) {
414
+ const absolutePath = path.join(root, relativePath);
415
+ if (!(await this.fileSystem.exists(absolutePath))) {
416
+ diagnostics.push({
417
+ severity: "error",
418
+ code: codes.missing,
419
+ message: `Configuration file is missing: ${relativePath}`,
420
+ path: relativePath,
421
+ });
422
+ return undefined;
423
+ }
424
+ try {
425
+ const result = schema.safeParse(parseYaml(await this.fileSystem.readText(absolutePath)));
426
+ if (result.success) {
427
+ return result.data;
428
+ }
429
+ diagnostics.push({
430
+ severity: "error",
431
+ code: codes.invalid,
432
+ message: result.error.message,
433
+ path: relativePath,
434
+ });
435
+ return undefined;
436
+ }
437
+ catch (error) {
438
+ diagnostics.push({
439
+ severity: "error",
440
+ code: codes.malformed,
441
+ message: error instanceof Error ? error.message : String(error),
442
+ path: relativePath,
443
+ });
444
+ return undefined;
445
+ }
446
+ }
447
+ }
448
+ function resolveSafeRelativePath(root, candidate, requiredPrefix) {
449
+ if (candidate.includes("\0") || path.win32.parse(candidate).root !== "")
450
+ return undefined;
451
+ const normalized = normalizeRelativePath(candidate);
452
+ if (path.posix.isAbsolute(normalized))
453
+ return undefined;
454
+ const segments = normalized.split("/");
455
+ if (segments.some((segment) => segment === ".."))
456
+ return undefined;
457
+ const absolute = path.resolve(root, ...segments);
458
+ const relativeToRoot = path.relative(root, absolute);
459
+ if (relativeToRoot === ".." ||
460
+ relativeToRoot.startsWith(".." + path.sep) ||
461
+ path.isAbsolute(relativeToRoot)) {
462
+ return undefined;
463
+ }
464
+ const comparison = normalizeForComparison(normalized);
465
+ if (requiredPrefix !== undefined &&
466
+ !isStrictDescendant(comparison, normalizeForComparison(requiredPrefix))) {
467
+ return undefined;
468
+ }
469
+ return { relative: normalized, absolute, comparison };
470
+ }
471
+ function isStrictDescendant(candidate, directory) {
472
+ return candidate.startsWith(directory + "/");
473
+ }
474
+ function isPathWithin(root, target) {
475
+ const relative = path.relative(root, target);
476
+ return (relative === "" ||
477
+ (relative !== ".." && !relative.startsWith(".." + path.sep) && !path.isAbsolute(relative)));
478
+ }
479
+ function normalizeRelativePath(value) {
480
+ const normalized = value
481
+ .trim()
482
+ .replaceAll("\\", "/")
483
+ .replace(/\/+/g, "/")
484
+ .replace(/^(?:\.\/)+/, "")
485
+ .replace(/\/$/, "");
486
+ return normalized || ".";
487
+ }
488
+ function normalizeForComparison(value) {
489
+ return normalizeRelativePath(value).toLowerCase();
490
+ }
491
+ function reportDuplicates(values, label, code, diagnostics, diagnosticPath = ".ai/workspace.yaml") {
492
+ const seen = new Set();
493
+ const reported = new Set();
494
+ for (const value of values) {
495
+ const key = value.toLowerCase();
496
+ if (seen.has(key) && !reported.has(key)) {
497
+ diagnostics.push({
498
+ severity: "error",
499
+ code,
500
+ message: `Duplicate ${label}: ${value}`,
501
+ path: diagnosticPath,
502
+ });
503
+ reported.add(key);
504
+ }
505
+ seen.add(key);
506
+ }
507
+ }
508
+ //# sourceMappingURL=doctor-service.js.map