@starci/skills 1.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 (321) hide show
  1. package/INDEX.md +74 -0
  2. package/INDEX.vi.md +75 -0
  3. package/README.md +44 -0
  4. package/README.vi.md +43 -0
  5. package/SKILL.md +135 -0
  6. package/SKILL.vi.md +128 -0
  7. package/alias/INDEX.md +104 -0
  8. package/alias/INDEX.vi.md +104 -0
  9. package/alias/alias.json +297 -0
  10. package/bin/starci-skills.mjs +219 -0
  11. package/knowledge/INDEX.md +22 -0
  12. package/knowledge/INDEX.vi.md +22 -0
  13. package/knowledge/grammars/starci/DNA.md +146 -0
  14. package/knowledge/grammars/starci/DNA.vi.md +146 -0
  15. package/knowledge/grammars/starci/INDEX.md +25 -0
  16. package/knowledge/grammars/starci/INDEX.vi.md +25 -0
  17. package/knowledge/grammars/starci/family.md +50 -0
  18. package/knowledge/grammars/starci/family.vi.md +50 -0
  19. package/knowledge/grammars/starci/idioms.md +120 -0
  20. package/knowledge/grammars/starci/idioms.vi.md +121 -0
  21. package/knowledge/grammars/starci/playbook.md +36 -0
  22. package/knowledge/grammars/starci/playbook.vi.md +35 -0
  23. package/knowledge/patterns/be/INDEX.md +29 -0
  24. package/knowledge/patterns/be/INDEX.vi.md +29 -0
  25. package/knowledge/patterns/be/comment.md +80 -0
  26. package/knowledge/patterns/be/comment.vi.md +80 -0
  27. package/knowledge/patterns/be/error.md +86 -0
  28. package/knowledge/patterns/be/error.vi.md +86 -0
  29. package/knowledge/patterns/be/folder.md +87 -0
  30. package/knowledge/patterns/be/folder.vi.md +86 -0
  31. package/knowledge/patterns/be/function.md +80 -0
  32. package/knowledge/patterns/be/function.vi.md +80 -0
  33. package/knowledge/patterns/be/imports.md +79 -0
  34. package/knowledge/patterns/be/imports.vi.md +79 -0
  35. package/knowledge/patterns/be/naming.md +87 -0
  36. package/knowledge/patterns/be/naming.vi.md +87 -0
  37. package/knowledge/patterns/be/test.md +79 -0
  38. package/knowledge/patterns/be/test.vi.md +79 -0
  39. package/knowledge/patterns/be/typing.md +73 -0
  40. package/knowledge/patterns/be/typing.vi.md +73 -0
  41. package/knowledge/patterns/fe/INDEX.md +29 -0
  42. package/knowledge/patterns/fe/INDEX.vi.md +29 -0
  43. package/knowledge/patterns/fe/comment.md +70 -0
  44. package/knowledge/patterns/fe/comment.vi.md +70 -0
  45. package/knowledge/patterns/fe/error.md +61 -0
  46. package/knowledge/patterns/fe/error.vi.md +61 -0
  47. package/knowledge/patterns/fe/folder.md +98 -0
  48. package/knowledge/patterns/fe/folder.vi.md +95 -0
  49. package/knowledge/patterns/fe/function.md +70 -0
  50. package/knowledge/patterns/fe/function.vi.md +70 -0
  51. package/knowledge/patterns/fe/imports.md +87 -0
  52. package/knowledge/patterns/fe/imports.vi.md +87 -0
  53. package/knowledge/patterns/fe/naming.md +77 -0
  54. package/knowledge/patterns/fe/naming.vi.md +77 -0
  55. package/knowledge/patterns/fe/test.md +73 -0
  56. package/knowledge/patterns/fe/test.vi.md +73 -0
  57. package/knowledge/patterns/fe/typing.md +67 -0
  58. package/knowledge/patterns/fe/typing.vi.md +67 -0
  59. package/knowledge/ui/INDEX.md +101 -0
  60. package/knowledge/ui/INDEX.vi.md +101 -0
  61. package/knowledge/ui/composition/INDEX.md +60 -0
  62. package/knowledge/ui/composition/INDEX.vi.md +63 -0
  63. package/knowledge/ui/composition/accent.md +73 -0
  64. package/knowledge/ui/composition/accent.vi.md +74 -0
  65. package/knowledge/ui/composition/action.md +55 -0
  66. package/knowledge/ui/composition/action.vi.md +56 -0
  67. package/knowledge/ui/composition/coverage.md +37 -0
  68. package/knowledge/ui/composition/coverage.vi.md +37 -0
  69. package/knowledge/ui/composition/cta.md +79 -0
  70. package/knowledge/ui/composition/cta.vi.md +79 -0
  71. package/knowledge/ui/composition/feedback.md +55 -0
  72. package/knowledge/ui/composition/feedback.vi.md +56 -0
  73. package/knowledge/ui/composition/hierarchy.md +83 -0
  74. package/knowledge/ui/composition/hierarchy.vi.md +82 -0
  75. package/knowledge/ui/composition/layout.md +91 -0
  76. package/knowledge/ui/composition/layout.vi.md +93 -0
  77. package/knowledge/ui/composition/responsive.md +67 -0
  78. package/knowledge/ui/composition/responsive.vi.md +67 -0
  79. package/knowledge/ui/composition/state.md +105 -0
  80. package/knowledge/ui/composition/state.vi.md +106 -0
  81. package/knowledge/ui/presentation/INDEX.md +94 -0
  82. package/knowledge/ui/presentation/INDEX.vi.md +69 -0
  83. package/knowledge/ui/presentation/boundary.md +170 -0
  84. package/knowledge/ui/presentation/boundary.vi.md +169 -0
  85. package/knowledge/ui/presentation/font.md +155 -0
  86. package/knowledge/ui/presentation/font.vi.md +156 -0
  87. package/knowledge/ui/presentation/gap.md +181 -0
  88. package/knowledge/ui/presentation/gap.vi.md +181 -0
  89. package/knowledge/ui/presentation/margin.md +168 -0
  90. package/knowledge/ui/presentation/margin.vi.md +171 -0
  91. package/knowledge/ui/presentation/measure.md +174 -0
  92. package/knowledge/ui/presentation/measure.vi.md +178 -0
  93. package/knowledge/ui/presentation/overflow.md +142 -0
  94. package/knowledge/ui/presentation/overflow.vi.md +142 -0
  95. package/knowledge/ui/presentation/padding.md +293 -0
  96. package/knowledge/ui/presentation/padding.vi.md +292 -0
  97. package/knowledge/ui/presentation/surface.md +166 -0
  98. package/knowledge/ui/presentation/surface.vi.md +166 -0
  99. package/knowledge/ui/presentation/text-flow.md +123 -0
  100. package/knowledge/ui/presentation/text-flow.vi.md +123 -0
  101. package/knowledge/ui/presentation/tone.md +114 -0
  102. package/knowledge/ui/presentation/tone.vi.md +113 -0
  103. package/knowledge/ui/proof/INDEX.md +55 -0
  104. package/knowledge/ui/proof/INDEX.vi.md +55 -0
  105. package/knowledge/ui/proof/accessibility.md +67 -0
  106. package/knowledge/ui/proof/accessibility.vi.md +68 -0
  107. package/knowledge/ui/proof/contrast.md +58 -0
  108. package/knowledge/ui/proof/contrast.vi.md +57 -0
  109. package/knowledge/ui/proof/focus.md +76 -0
  110. package/knowledge/ui/proof/focus.vi.md +76 -0
  111. package/knowledge/ui/proof/motion.md +63 -0
  112. package/knowledge/ui/proof/motion.vi.md +64 -0
  113. package/knowledge/ui/proof/render-truth.md +63 -0
  114. package/knowledge/ui/proof/render-truth.vi.md +63 -0
  115. package/operators/INDEX.md +199 -0
  116. package/operators/INDEX.vi.md +199 -0
  117. package/operators/architecture-decide/errors.json +115 -0
  118. package/operators/architecture-decide/operator.json +20 -0
  119. package/operators/architecture-decide/operator.md +133 -0
  120. package/operators/architecture-decide/operator.vi.md +130 -0
  121. package/operators/architecture-decide/self-test.mjs +221 -0
  122. package/operators/architecture-decide/validate.mjs +112 -0
  123. package/operators/backend-source-apply/errors.json +66 -0
  124. package/operators/backend-source-apply/operator.json +20 -0
  125. package/operators/backend-source-apply/operator.md +189 -0
  126. package/operators/backend-source-apply/operator.vi.md +185 -0
  127. package/operators/backend-source-apply/self-test.mjs +257 -0
  128. package/operators/backend-source-apply/validate.mjs +240 -0
  129. package/operators/business-decide/errors.json +78 -0
  130. package/operators/business-decide/operator.json +20 -0
  131. package/operators/business-decide/operator.md +176 -0
  132. package/operators/business-decide/operator.vi.md +174 -0
  133. package/operators/business-decide/self-test.mjs +225 -0
  134. package/operators/business-decide/validate.mjs +277 -0
  135. package/operators/content-generate/errors.json +106 -0
  136. package/operators/content-generate/operator.json +21 -0
  137. package/operators/content-generate/operator.md +155 -0
  138. package/operators/content-generate/operator.vi.md +155 -0
  139. package/operators/content-generate/self-test.mjs +288 -0
  140. package/operators/content-generate/validate.mjs +196 -0
  141. package/operators/errors.json +178 -0
  142. package/operators/frontend-direction-decide/errors.json +135 -0
  143. package/operators/frontend-direction-decide/operator.json +21 -0
  144. package/operators/frontend-direction-decide/operator.md +167 -0
  145. package/operators/frontend-direction-decide/operator.vi.md +167 -0
  146. package/operators/frontend-direction-decide/self-test.mjs +226 -0
  147. package/operators/frontend-direction-decide/validate.mjs +146 -0
  148. package/operators/frontend-presentation-resolve/errors.json +42 -0
  149. package/operators/frontend-presentation-resolve/operator.json +19 -0
  150. package/operators/frontend-presentation-resolve/operator.md +142 -0
  151. package/operators/frontend-presentation-resolve/operator.vi.md +140 -0
  152. package/operators/frontend-presentation-resolve/self-test.mjs +163 -0
  153. package/operators/frontend-presentation-resolve/validate.mjs +143 -0
  154. package/operators/frontend-source-apply/errors.json +30 -0
  155. package/operators/frontend-source-apply/operator.json +20 -0
  156. package/operators/frontend-source-apply/operator.md +127 -0
  157. package/operators/frontend-source-apply/operator.vi.md +126 -0
  158. package/operators/frontend-source-apply/self-test.mjs +214 -0
  159. package/operators/frontend-source-apply/validate.mjs +133 -0
  160. package/operators/frontend-surface-audit/errors.json +5 -0
  161. package/operators/frontend-surface-audit/operator.json +22 -0
  162. package/operators/frontend-surface-audit/operator.md +121 -0
  163. package/operators/frontend-surface-audit/operator.vi.md +121 -0
  164. package/operators/frontend-surface-audit/self-test.mjs +185 -0
  165. package/operators/frontend-surface-audit/validate.mjs +127 -0
  166. package/operators/git-publish/errors.json +54 -0
  167. package/operators/git-publish/operator.json +20 -0
  168. package/operators/git-publish/operator.md +180 -0
  169. package/operators/git-publish/operator.vi.md +178 -0
  170. package/operators/git-publish/self-test.mjs +177 -0
  171. package/operators/git-publish/validate.mjs +106 -0
  172. package/operators/platform-operate/errors.json +90 -0
  173. package/operators/platform-operate/operator.json +22 -0
  174. package/operators/platform-operate/operator.md +162 -0
  175. package/operators/platform-operate/operator.vi.md +160 -0
  176. package/operators/platform-operate/self-test.mjs +202 -0
  177. package/operators/platform-operate/validate.mjs +193 -0
  178. package/operators/quality-verify/errors.json +54 -0
  179. package/operators/quality-verify/operator.json +20 -0
  180. package/operators/quality-verify/operator.md +185 -0
  181. package/operators/quality-verify/operator.vi.md +177 -0
  182. package/operators/quality-verify/self-test.mjs +210 -0
  183. package/operators/quality-verify/validate.mjs +197 -0
  184. package/operators/release-deploy/errors.json +158 -0
  185. package/operators/release-deploy/operator.json +23 -0
  186. package/operators/release-deploy/operator.md +183 -0
  187. package/operators/release-deploy/operator.vi.md +181 -0
  188. package/operators/release-deploy/self-test.mjs +235 -0
  189. package/operators/release-deploy/validate.mjs +129 -0
  190. package/operators/uat-verify/errors.json +78 -0
  191. package/operators/uat-verify/operator.json +25 -0
  192. package/operators/uat-verify/operator.md +162 -0
  193. package/operators/uat-verify/operator.vi.md +161 -0
  194. package/operators/uat-verify/self-test.mjs +270 -0
  195. package/operators/uat-verify/validate.mjs +202 -0
  196. package/operators/workspace-bind/errors.json +90 -0
  197. package/operators/workspace-bind/operator.json +21 -0
  198. package/operators/workspace-bind/operator.md +148 -0
  199. package/operators/workspace-bind/operator.vi.md +148 -0
  200. package/operators/workspace-bind/self-test.mjs +205 -0
  201. package/operators/workspace-bind/validate.mjs +152 -0
  202. package/package.json +50 -0
  203. package/readiness/initialization/workspaces/commit-policy.json +91 -0
  204. package/readiness/initialization/workspaces/config.schema.json +35 -0
  205. package/readiness/initialization/workspaces/device-state.schema.json +54 -0
  206. package/readiness/initialization/workspaces/local-route.schema.json +206 -0
  207. package/readiness/initialization/workspaces/portable-route.schema.json +200 -0
  208. package/resources/INDEX.md +96 -0
  209. package/resources/INDEX.vi.md +99 -0
  210. package/resources/agents/profiles/claude.json +131 -0
  211. package/resources/agents/profiles/openai.json +131 -0
  212. package/resources/orchestrator.json +71 -0
  213. package/resources/tools.json +85 -0
  214. package/routing.json +272 -0
  215. package/scripts/alias-registry.mjs +31 -0
  216. package/scripts/device-state.mjs +497 -0
  217. package/scripts/device-state.spec.mjs +18 -0
  218. package/scripts/errors-registry.mjs +68 -0
  219. package/scripts/generate-alias-doc.mjs +66 -0
  220. package/scripts/generate-grammar-dna.mjs +387 -0
  221. package/scripts/generate-operators-index.mjs +95 -0
  222. package/scripts/generate-presentation-owned.mjs +681 -0
  223. package/scripts/install-cli.spec.mjs +74 -0
  224. package/scripts/json-schema.mjs +94 -0
  225. package/scripts/operator-md.mjs +96 -0
  226. package/scripts/run-operator-self-tests.mjs +36 -0
  227. package/scripts/validate-alias.mjs +165 -0
  228. package/scripts/validate-defaults.mjs +72 -0
  229. package/scripts/validate-knowledge-citations.mjs +90 -0
  230. package/scripts/validate-operator.mjs +125 -0
  231. package/scripts/validate-request.mjs +80 -0
  232. package/scripts/validate-resources.mjs +117 -0
  233. package/scripts/validate-response.mjs +141 -0
  234. package/scripts/validate-routing.mjs +91 -0
  235. package/scripts/validate-step.mjs +50 -0
  236. package/scripts/validate-templates.mjs +226 -0
  237. package/scripts/validate-templates.spec.mjs +144 -0
  238. package/scripts/validate-workflows.mjs +106 -0
  239. package/scripts/workspace-portable.mjs +389 -0
  240. package/scripts/workspace-portable.spec.mjs +246 -0
  241. package/templates/README.md +47 -0
  242. package/templates/README.vi.md +45 -0
  243. package/templates/changes.example.md +27 -0
  244. package/templates/grammars.template.md +34 -0
  245. package/templates/kinds/architecture-decision.contract.json +14 -0
  246. package/templates/kinds/architecture-decision.skeleton.md +52 -0
  247. package/templates/kinds/backend-source-application.contract.json +10 -0
  248. package/templates/kinds/backend-source-application.skeleton.md +34 -0
  249. package/templates/kinds/business-promise-authority.contract.json +13 -0
  250. package/templates/kinds/business-promise-authority.skeleton.md +67 -0
  251. package/templates/kinds/capture.schema.json +33 -0
  252. package/templates/kinds/changes.contract.json +10 -0
  253. package/templates/kinds/changes.skeleton.md +26 -0
  254. package/templates/kinds/checks.schema.json +44 -0
  255. package/templates/kinds/claims.schema.json +165 -0
  256. package/templates/kinds/conformance.schema.json +15 -0
  257. package/templates/kinds/content-brief.contract.json +12 -0
  258. package/templates/kinds/content-brief.skeleton.md +45 -0
  259. package/templates/kinds/content-generation-receipt.contract.json +11 -0
  260. package/templates/kinds/content-generation-receipt.skeleton.md +40 -0
  261. package/templates/kinds/content-review.contract.json +11 -0
  262. package/templates/kinds/content-review.skeleton.md +45 -0
  263. package/templates/kinds/contract.schema.json +28 -0
  264. package/templates/kinds/coverage-matrix.schema.json +86 -0
  265. package/templates/kinds/coverage.schema.json +62 -0
  266. package/templates/kinds/current-state.schema.json +40 -0
  267. package/templates/kinds/delta.schema.json +95 -0
  268. package/templates/kinds/e2e.schema.json +63 -0
  269. package/templates/kinds/frontend-direction-decision.contract.json +93 -0
  270. package/templates/kinds/frontend-direction-decision.skeleton.md +68 -0
  271. package/templates/kinds/frontend-presentation-resolution.contract.json +11 -0
  272. package/templates/kinds/frontend-presentation-resolution.skeleton.md +32 -0
  273. package/templates/kinds/frontend-source-application.contract.json +10 -0
  274. package/templates/kinds/frontend-source-application.skeleton.md +30 -0
  275. package/templates/kinds/frontend-surface-audit.contract.json +52 -0
  276. package/templates/kinds/frontend-surface-audit.skeleton.md +32 -0
  277. package/templates/kinds/gate-result.schema.json +64 -0
  278. package/templates/kinds/git-publication.contract.json +82 -0
  279. package/templates/kinds/git-publication.skeleton.md +61 -0
  280. package/templates/kinds/independent-critique.contract.json +9 -0
  281. package/templates/kinds/independent-critique.skeleton.md +28 -0
  282. package/templates/kinds/inventory.schema.json +35 -0
  283. package/templates/kinds/model.schema.json +99 -0
  284. package/templates/kinds/mutations.schema.json +65 -0
  285. package/templates/kinds/platform-operation-receipt.contract.json +76 -0
  286. package/templates/kinds/platform-operation-receipt.skeleton.md +54 -0
  287. package/templates/kinds/probes.schema.json +130 -0
  288. package/templates/kinds/proof.schema.json +17 -0
  289. package/templates/kinds/quality-verification.contract.json +87 -0
  290. package/templates/kinds/quality-verification.skeleton.md +58 -0
  291. package/templates/kinds/release-deployment.contract.json +85 -0
  292. package/templates/kinds/release-deployment.skeleton.md +67 -0
  293. package/templates/kinds/route.schema.json +293 -0
  294. package/templates/kinds/stack-model.schema.json +90 -0
  295. package/templates/kinds/uat-capture.schema.json +35 -0
  296. package/templates/kinds/uat-flow-verification.contract.json +11 -0
  297. package/templates/kinds/uat-flow-verification.skeleton.md +47 -0
  298. package/templates/kinds/uat-snapshot.schema.json +96 -0
  299. package/templates/kinds/uat-verdicts.schema.json +41 -0
  300. package/templates/kinds/ui-coverage.schema.json +77 -0
  301. package/templates/kinds/verdicts.schema.json +39 -0
  302. package/templates/kinds/workspace-route-binding.contract.json +12 -0
  303. package/templates/kinds/workspace-route-binding.skeleton.md +60 -0
  304. package/templates/kinds/writes.schema.json +37 -0
  305. package/templates/operator.template.md +73 -0
  306. package/templates/patterns.template.md +31 -0
  307. package/templates/step/request.schema.json +42 -0
  308. package/templates/step/response.schema.json +144 -0
  309. package/templates/ui-composition.template.md +36 -0
  310. package/templates/ui-presentation.template.md +57 -0
  311. package/templates/ui-proof.template.md +34 -0
  312. package/workflows/README.md +37 -0
  313. package/workflows/README.vi.md +37 -0
  314. package/workflows/backend-feature.json +59 -0
  315. package/workflows/content-unit.json +19 -0
  316. package/workflows/frontend-new-surface.json +81 -0
  317. package/workflows/frontend-reconstruct.json +67 -0
  318. package/workflows/frontend-refine.json +67 -0
  319. package/workflows/frontend-with-uat.json +78 -0
  320. package/workflows/full-feature.json +104 -0
  321. package/workflows/release.json +29 -0
@@ -0,0 +1,389 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execFileSync } from 'node:child_process';
4
+ import {
5
+ existsSync,
6
+ mkdirSync,
7
+ readFileSync,
8
+ readdirSync,
9
+ realpathSync,
10
+ renameSync,
11
+ rmSync,
12
+ statSync,
13
+ writeFileSync
14
+ } from 'node:fs';
15
+ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
16
+ import { fileURLToPath } from 'node:url';
17
+ import { validatorFor } from './json-schema.mjs';
18
+
19
+ const scriptRoot = dirname(fileURLToPath(import.meta.url));
20
+ const starciRoot = resolve(scriptRoot, '..');
21
+ const defaultSource = resolve(starciRoot, '..');
22
+ const portableSchemaPath = join(starciRoot, 'readiness', 'initialization', 'workspaces', 'portable-route.schema.json');
23
+ const localSchemaPath = join(starciRoot, 'readiness', 'initialization', 'workspaces', 'local-route.schema.json');
24
+ const configSchemaPath = join(starciRoot, 'readiness', 'initialization', 'workspaces', 'config.schema.json');
25
+ const validatePortableSchema = validatorFor(new URL('../readiness/initialization/workspaces/portable-route.schema.json', import.meta.url));
26
+ const validateLocalSchema = validatorFor(new URL('../readiness/initialization/workspaces/local-route.schema.json', import.meta.url));
27
+ const validateConfigSchema = validatorFor(new URL('../readiness/initialization/workspaces/config.schema.json', import.meta.url));
28
+ const githubRemote = /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:\.git)?$/;
29
+
30
+ const slash = (value) => value.split(sep).join('/');
31
+ const stableJson = (value) => `${JSON.stringify(value, null, 2)}\n`;
32
+ const readJson = (path) => JSON.parse(readFileSync(path, 'utf8'));
33
+
34
+ function fail(message) {
35
+ throw new Error(message);
36
+ }
37
+
38
+ function value(args, flag) {
39
+ const index = args.indexOf(flag);
40
+ return index < 0 ? undefined : args[index + 1];
41
+ }
42
+
43
+ function samePath(left, right) {
44
+ const normalize = (path) => resolve(path).replaceAll('\\', '/').replace(/\/$/, '').toLowerCase();
45
+ return normalize(left) === normalize(right);
46
+ }
47
+
48
+ function inside(root, target) {
49
+ const result = relative(resolve(root), resolve(target));
50
+ return result !== '' && result !== '..' && !result.startsWith(`..${sep}`) && !isAbsolute(result);
51
+ }
52
+
53
+ function normalizeRemote(remote) {
54
+ return remote.trim().replace(/\.git$/i, '').replace(/\/$/, '').toLowerCase();
55
+ }
56
+
57
+ function git(repository, ...args) {
58
+ try {
59
+ return execFileSync('git', ['-C', repository, ...args], {
60
+ encoding: 'utf8',
61
+ windowsHide: true
62
+ }).trim();
63
+ } catch {
64
+ fail(`Git verification failed at ${repository}: git ${args.join(' ')}`);
65
+ }
66
+ }
67
+
68
+ function assertSchemaLink(documentPath, schemaRef) {
69
+ if (typeof schemaRef !== 'string' || schemaRef.length === 0) fail(`${documentPath} has no $schema`);
70
+ const target = resolve(dirname(documentPath), schemaRef);
71
+ if (!existsSync(target)) fail(`${documentPath} references missing schema ${target}`);
72
+ return target;
73
+ }
74
+
75
+ function assertPublicationSafe(document, label) {
76
+ const forbiddenKeys = new Set(['diskPath', 'gitRoot', 'head', 'updatedAt']);
77
+ const visit = (current, at) => {
78
+ if (typeof current === 'string') {
79
+ if (/credential|token|secret|password/i.test(current)) fail(`${at} contains secret-bearing text`);
80
+ if (/^[A-Za-z]:[\\/]/.test(current) || current.startsWith('/')) fail(`${at} contains an absolute path`);
81
+ if (/^https?:\/\/[^/\s]+@/i.test(current)) fail(`${at} contains URL user information`);
82
+ return;
83
+ }
84
+ if (Array.isArray(current)) {
85
+ current.forEach((item, index) => visit(item, `${at}[${index}]`));
86
+ return;
87
+ }
88
+ if (!current || typeof current !== 'object') return;
89
+ for (const [key, child] of Object.entries(current)) {
90
+ if (forbiddenKeys.has(key)) fail(`${at}.${key} contains observed machine state`);
91
+ if (/credential|token|secret|password/i.test(key)) fail(`${at}.${key} is secret-bearing`);
92
+ visit(child, `${at}.${key}`);
93
+ }
94
+ };
95
+ visit(document, label);
96
+ }
97
+
98
+ export function validatePortableRoute(route, label = 'portable route') {
99
+ const result = validatePortableSchema(route);
100
+ if (!result.valid) fail(`${label} is invalid:\n${result.errors.join('\n')}`);
101
+ if (!githubRemote.test(route.repository.gitRepository)) fail(`${label} has an invalid GitHub origin`);
102
+ if (route.schemaVersion === 6 && route.role !== 'fe' && route.context.grammarId !== null) {
103
+ fail(`${label} non-FE grammarId must be null`);
104
+ }
105
+ if (route.repository.gitPolicy?.mutationBranch !== undefined
106
+ && route.repository.gitPolicy.mutationBranch !== route.repository.branch) {
107
+ fail(`${label} gitPolicy.mutationBranch must equal repository.branch`);
108
+ }
109
+ assertPublicationSafe(route, label);
110
+ return route;
111
+ }
112
+
113
+ export function validateLocalRoute(route, label = 'local route') {
114
+ const result = validateLocalSchema(route);
115
+ if (!result.valid) fail(`${label} is invalid:\n${result.errors.join('\n')}`);
116
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/.test(route.updatedAt)) {
117
+ fail(`${label}.updatedAt must be an ISO UTC timestamp`);
118
+ }
119
+ if (route.schemaVersion === 6 && route.role !== 'fe' && route.context.grammarId !== null) {
120
+ fail(`${label} non-FE grammarId must be null`);
121
+ }
122
+ if (route.repository.gitPolicy?.mutationBranch !== undefined
123
+ && route.repository.gitPolicy.mutationBranch !== route.repository.branch) {
124
+ fail(`${label} gitPolicy.mutationBranch must equal repository.branch`);
125
+ }
126
+ return route;
127
+ }
128
+
129
+ function directories(root) {
130
+ if (!existsSync(root)) return [];
131
+ return readdirSync(root, { withFileTypes: true })
132
+ .filter((entry) => entry.isDirectory())
133
+ .map((entry) => entry.name)
134
+ .sort();
135
+ }
136
+
137
+ function jsonFiles(root) {
138
+ if (!existsSync(root)) return [];
139
+ return readdirSync(root, { withFileTypes: true })
140
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
141
+ .map((entry) => entry.name)
142
+ .sort();
143
+ }
144
+
145
+ function loadDeclarations(source) {
146
+ const workspaceRoot = join(source, '.workspaces');
147
+ const configPath = join(workspaceRoot, 'config.json');
148
+ if (!existsSync(configPath)) fail(`workspace config is absent: ${configPath}`);
149
+ assertSchemaLink(configPath, readJson(configPath).$schema);
150
+ const configResult = validateConfigSchema(readJson(configPath));
151
+ if (!configResult.valid) fail(`workspace config is invalid:\n${configResult.errors.join('\n')}`);
152
+
153
+ const projectsRoot = join(workspaceRoot, 'projects');
154
+ const declarations = [];
155
+ for (const project of directories(projectsRoot)) {
156
+ for (const file of jsonFiles(join(projectsRoot, project))) {
157
+ const path = join(projectsRoot, project, file);
158
+ const route = readJson(path);
159
+ assertSchemaLink(path, route.$schema);
160
+ validatePortableRoute(route, slash(relative(source, path)));
161
+ if (route.project !== project || `${route.role}.json` !== file) fail(`${path} identity does not match its location`);
162
+ declarations.push({ path, route });
163
+ }
164
+ }
165
+ if (declarations.length === 0) fail(`portable workspace declarations are absent under ${projectsRoot}`);
166
+ return { workspaceRoot, declarations };
167
+ }
168
+
169
+ function resolveContextFile(repository, portablePath, label) {
170
+ const candidate = resolve(repository, portablePath);
171
+ if (!existsSync(candidate)) fail(`${label} is absent: ${candidate}`);
172
+ const actual = realpathSync(candidate);
173
+ if (!inside(repository, actual)) fail(`${label} escapes repository: ${portablePath}`);
174
+ return actual;
175
+ }
176
+
177
+ function buildLocalRoute({ source, repositoriesRoot, workspaceRoot, declaration }) {
178
+ const { route } = declaration;
179
+ const repositoryCandidate = route.repository.kind === 'source'
180
+ ? source
181
+ : resolve(repositoriesRoot, route.repository.directory);
182
+ if (!existsSync(repositoryCandidate) || !statSync(repositoryCandidate).isDirectory()) {
183
+ fail(`${route.project}/${route.role} checkout is absent: ${repositoryCandidate}`);
184
+ }
185
+ const repository = realpathSync(repositoryCandidate);
186
+ if (route.repository.kind === 'source') {
187
+ if (!samePath(repository, source)) fail(`${route.project}/${route.role} source route resolved outside Source`);
188
+ } else if (!inside(repositoriesRoot, repository)) {
189
+ fail(`${route.project}/${route.role} checkout escapes repositories root`);
190
+ }
191
+ const gitRoot = realpathSync(git(repository, 'rev-parse', '--show-toplevel'));
192
+ if (!samePath(gitRoot, repository)) fail(`${route.project}/${route.role} checkout is not its Git root`);
193
+ const observedRemote = git(repository, 'config', '--get', 'remote.origin.url');
194
+ if (normalizeRemote(observedRemote) !== normalizeRemote(route.repository.gitRepository)) {
195
+ fail(`${route.project}/${route.role} origin mismatch`);
196
+ }
197
+ const branch = git(repository, 'branch', '--show-current');
198
+ if (branch !== route.repository.branch) fail(`${route.project}/${route.role} branch mismatch: expected ${route.repository.branch}, observed ${branch}`);
199
+
200
+ const context = route.context;
201
+ const absoluteList = (paths, label) => paths.map((path, index) => resolveContextFile(repository, path, `${route.project}/${route.role} ${label}[${index}]`));
202
+ const commonContext = {
203
+ instructions: absoluteList(context.instructions, 'instruction'),
204
+ manifests: absoluteList(context.manifests, 'manifest')
205
+ };
206
+ const v6 = route.schemaVersion === 6;
207
+ const local = {
208
+ $schema: '../../../../../.claude/readiness/initialization/workspaces/local-route.schema.json',
209
+ ...(v6 ? { schemaVersion: 6, schemaRevision: 2 } : { version: 1 }),
210
+ project: route.project,
211
+ role: route.role,
212
+ source: {
213
+ path: source,
214
+ trust: join(source, '.claude'),
215
+ skills: join(source, '.claude', 'skills'),
216
+ workspaceRoot
217
+ },
218
+ repository: {
219
+ diskPath: repository,
220
+ gitRoot: repository,
221
+ gitRepository: route.repository.gitRepository,
222
+ branch,
223
+ head: git(repository, 'rev-parse', 'HEAD'),
224
+ ...(route.repository.gitPolicy ? { gitPolicy: route.repository.gitPolicy } : {})
225
+ },
226
+ context: v6
227
+ ? {
228
+ ...commonContext,
229
+ contract: (context.contract ?? null) === null
230
+ ? null
231
+ : resolveContextFile(repository, context.contract, `${route.project}/${route.role} contract`),
232
+ contractSource: context.contractSource ?? null,
233
+ grammarId: context.grammarId
234
+ }
235
+ : { ...commonContext, grammar: context.grammar, grammarProfile: context.grammarProfile },
236
+ updatedAt: new Date().toISOString()
237
+ };
238
+ validateLocalRoute(local, `${route.project}/${route.role} hydrated route`);
239
+ return {
240
+ target: join(workspaceRoot, 'local', 'routes', route.project, route.role, 'config.json'),
241
+ value: local
242
+ };
243
+ }
244
+
245
+ function comparable(route) {
246
+ const value = structuredClone(route);
247
+ delete value.updatedAt;
248
+ return value;
249
+ }
250
+
251
+ function changed(target, expected) {
252
+ if (!existsSync(target)) return true;
253
+ try {
254
+ const actual = readJson(target);
255
+ return JSON.stringify(comparable(actual)) !== JSON.stringify(comparable(expected));
256
+ } catch {
257
+ return true;
258
+ }
259
+ }
260
+
261
+ function atomicWrite(target, value) {
262
+ mkdirSync(dirname(target), { recursive: true });
263
+ const temporary = join(dirname(target), `.${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.tmp`);
264
+ try {
265
+ writeFileSync(temporary, stableJson(value), { encoding: 'utf8', flag: 'wx' });
266
+ renameSync(temporary, target);
267
+ } finally {
268
+ if (existsSync(temporary)) rmSync(temporary, { force: true });
269
+ }
270
+ }
271
+
272
+ function checkoutPlans(repositoriesRoot, declarations) {
273
+ const byDirectory = new Map();
274
+ for (const { route } of declarations) {
275
+ if (route.repository.kind === 'source') continue;
276
+ const key = route.repository.directory;
277
+ const previous = byDirectory.get(key);
278
+ if (previous && (
279
+ normalizeRemote(previous.gitRepository) !== normalizeRemote(route.repository.gitRepository)
280
+ || previous.branch !== route.repository.branch
281
+ )) fail(`conflicting checkout declarations for ${key}`);
282
+ byDirectory.set(key, route.repository);
283
+ }
284
+ return [...byDirectory.values()].sort((left, right) => left.directory.localeCompare(right.directory)).map((repository) => {
285
+ const target = resolve(repositoriesRoot, repository.directory);
286
+ if (!inside(repositoriesRoot, target)) fail(`checkout target escapes repositories root: ${repository.directory}`);
287
+ return { ...repository, target };
288
+ });
289
+ }
290
+
291
+ function checkoutGitEnvironment(source, credentialFile) {
292
+ const env = { ...process.env, GIT_TERMINAL_PROMPT: '0' };
293
+ if (!credentialFile) return env;
294
+ const path = resolve(source, credentialFile);
295
+ if (!inside(source, path) || !existsSync(path)) fail(`checkout credential file is absent or outside Source: ${credentialFile}`);
296
+ const token = readFileSync(path, 'utf8').trim();
297
+ if (token === '') fail(`checkout credential file is empty: ${credentialFile}`);
298
+ const parsedCount = Number.parseInt(env.GIT_CONFIG_COUNT ?? '0', 10);
299
+ const index = Number.isSafeInteger(parsedCount) && parsedCount >= 0 ? parsedCount : 0;
300
+ env.GIT_CONFIG_COUNT = String(index + 1);
301
+ env[`GIT_CONFIG_KEY_${index}`] = 'http.https://github.com/.extraheader';
302
+ env[`GIT_CONFIG_VALUE_${index}`] = `Authorization: Basic ${Buffer.from(`x-access-token:${token}`, 'utf8').toString('base64')}`;
303
+ return env;
304
+ }
305
+
306
+ function initializeCheckouts(plans, env) {
307
+ const initialized = [];
308
+ for (const plan of plans) {
309
+ if (existsSync(plan.target)) continue;
310
+ try {
311
+ execFileSync('git', [
312
+ 'clone',
313
+ '--quiet',
314
+ '--single-branch',
315
+ '--branch',
316
+ plan.branch,
317
+ '--',
318
+ plan.gitRepository,
319
+ plan.target
320
+ ], { encoding: 'utf8', env, windowsHide: true });
321
+ execFileSync('git', ['-C', plan.target, 'remote', 'set-url', 'origin', plan.gitRepository], {
322
+ encoding: 'utf8',
323
+ env,
324
+ windowsHide: true
325
+ });
326
+ } catch {
327
+ fail(`checkout initialization failed: ${plan.gitRepository} -> ${plan.target}`);
328
+ }
329
+ initialized.push(plan.target);
330
+ }
331
+ return initialized;
332
+ }
333
+
334
+ export function run(argv = process.argv.slice(2)) {
335
+ const command = argv[0];
336
+ if (!['bootstrap', 'hydrate', 'check'].includes(command)) fail('Usage: workspace-portable.mjs <bootstrap|hydrate|check> --source <Source> [--repositories-root <path>] [--project <id>] [--credential-file <Source-relative path>] [--plan|--apply]');
337
+ const plan = argv.includes('--plan');
338
+ const apply = argv.includes('--apply');
339
+ if (['bootstrap', 'hydrate'].includes(command) && plan === apply) fail(`${command} requires exactly one of --plan or --apply`);
340
+ if (command === 'check' && (plan || apply)) fail('check accepts neither --plan nor --apply');
341
+
342
+ if (!existsSync(portableSchemaPath) || !existsSync(localSchemaPath) || !existsSync(configSchemaPath)) fail('V6 workspace schemas are incomplete');
343
+ const source = realpathSync(resolve(value(argv, '--source') ?? defaultSource));
344
+ const repositoriesRoot = realpathSync(resolve(value(argv, '--repositories-root') ?? dirname(source)));
345
+ const project = value(argv, '--project');
346
+ const credentialFile = value(argv, '--credential-file');
347
+ if (credentialFile && command !== 'bootstrap') fail('--credential-file is accepted only by bootstrap');
348
+ const { workspaceRoot, declarations } = loadDeclarations(source);
349
+ const selected = project ? declarations.filter((item) => item.route.project === project) : declarations;
350
+ if (selected.length === 0) fail(`no workspace declarations selected${project ? ` for ${project}` : ''}`);
351
+
352
+ const checkoutPlan = command === 'bootstrap' ? checkoutPlans(repositoriesRoot, selected) : [];
353
+ const missingCheckouts = checkoutPlan.filter((item) => !existsSync(item.target));
354
+ if (command === 'bootstrap' && plan && missingCheckouts.length > 0) {
355
+ console.log(JSON.stringify({
356
+ status: 'initialize-required',
357
+ routeCount: selected.length,
358
+ missingCheckouts: missingCheckouts.map((item) => slash(relative(repositoriesRoot, item.target))),
359
+ changedRoutes: []
360
+ }));
361
+ return 0;
362
+ }
363
+ const initializedCheckouts = command === 'bootstrap' && apply
364
+ ? initializeCheckouts(checkoutPlan, checkoutGitEnvironment(source, credentialFile))
365
+ : [];
366
+
367
+ const expected = selected.map((declaration) => buildLocalRoute({ source, repositoriesRoot, workspaceRoot, declaration }));
368
+ const changes = expected.filter((item) => changed(item.target, item.value));
369
+ if ((command === 'hydrate' || command === 'bootstrap') && apply) changes.forEach((item) => atomicWrite(item.target, item.value));
370
+ const summary = {
371
+ status: command === 'check' && changes.length > 0 ? 'stale' : 'ready',
372
+ routeCount: expected.length,
373
+ ...(command === 'bootstrap' ? {
374
+ initializedCheckouts: initializedCheckouts.map((item) => slash(relative(repositoriesRoot, item)))
375
+ } : {}),
376
+ changedRoutes: changes.map((item) => slash(relative(source, item.target)))
377
+ };
378
+ console.log(JSON.stringify(summary));
379
+ return command === 'check' && changes.length > 0 ? 1 : 0;
380
+ }
381
+
382
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
383
+ try {
384
+ process.exitCode = run();
385
+ } catch (error) {
386
+ console.error(error.message);
387
+ process.exitCode = 1;
388
+ }
389
+ }
@@ -0,0 +1,246 @@
1
+ import assert from 'node:assert/strict';
2
+ import { execFileSync, spawnSync } from 'node:child_process';
3
+ import {
4
+ copyFileSync,
5
+ existsSync,
6
+ mkdirSync,
7
+ mkdtempSync,
8
+ readFileSync,
9
+ realpathSync,
10
+ rmSync,
11
+ writeFileSync
12
+ } from 'node:fs';
13
+ import { tmpdir } from 'node:os';
14
+ import { dirname, join } from 'node:path';
15
+ import { fileURLToPath, pathToFileURL } from 'node:url';
16
+ import test from 'node:test';
17
+ import { validatePortableRoute } from './workspace-portable.mjs';
18
+
19
+ const script = fileURLToPath(new URL('./workspace-portable.mjs', import.meta.url));
20
+ const schemaRoot = fileURLToPath(new URL('../readiness/initialization/workspaces/', import.meta.url));
21
+
22
+ function writeJson(path, value) {
23
+ mkdirSync(dirname(path), { recursive: true });
24
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
25
+ }
26
+
27
+ function git(repository, ...args) {
28
+ return execFileSync('git', ['-C', repository, ...args], { encoding: 'utf8', windowsHide: true }).trim();
29
+ }
30
+
31
+ function repository(path, remote) {
32
+ mkdirSync(path, { recursive: true });
33
+ execFileSync('git', ['init', '-b', 'main', path], { stdio: 'ignore', windowsHide: true });
34
+ git(path, 'config', 'user.name', 'Test');
35
+ git(path, 'config', 'user.email', 'test@example.com');
36
+ git(path, 'remote', 'add', 'origin', remote);
37
+ writeFileSync(join(path, 'package.json'), '{}\n');
38
+ git(path, 'add', 'package.json');
39
+ git(path, 'commit', '-m', 'fixture');
40
+ }
41
+
42
+ function fixture(t) {
43
+ const repositories = mkdtempSync(join(tmpdir(), 'starci-v6-workspaces-'));
44
+ const source = join(repositories, 'source');
45
+ const frontend = join(repositories, 'nivo-fe');
46
+ t.after(() => rmSync(repositories, { recursive: true, force: true }));
47
+ repository(source, 'https://github.com/starci-lab/source.git');
48
+ repository(frontend, 'https://github.com/starci-lab/nivo-fe.git');
49
+ const frontendContract = join(frontend, 'packages', 'ui', 'src', 'contracts', 'index.ts');
50
+ mkdirSync(dirname(frontendContract), { recursive: true });
51
+ writeFileSync(frontendContract, 'export const CONTRACTS = {};\n');
52
+ git(frontend, 'add', 'packages/ui/src/contracts/index.ts');
53
+ git(frontend, 'commit', '-m', 'contract fixture');
54
+ mkdirSync(join(source, '.claude', 'readiness', 'initialization', 'workspaces'), { recursive: true });
55
+ for (const name of ['config.schema.json', 'portable-route.schema.json', 'local-route.schema.json']) {
56
+ copyFileSync(join(schemaRoot, name), join(source, '.claude', 'readiness', 'initialization', 'workspaces', name));
57
+ }
58
+ mkdirSync(join(source, '.claude', 'skills'), { recursive: true });
59
+ writeJson(join(source, '.workspaces', 'config.json'), {
60
+ $schema: '../.claude/readiness/initialization/workspaces/config.schema.json',
61
+ schemaVersion: 6,
62
+ defaultLang: 'vi'
63
+ });
64
+ writeJson(join(source, '.workspaces', 'projects', 'source', 'be.json'), {
65
+ $schema: '../../../.claude/readiness/initialization/workspaces/portable-route.schema.json',
66
+ version: 1,
67
+ project: 'source',
68
+ role: 'be',
69
+ repository: { kind: 'source', directory: null, gitRepository: 'https://github.com/starci-lab/source.git', branch: 'main' },
70
+ context: { instructions: [], manifests: ['package.json'], grammar: null, grammarProfile: null }
71
+ });
72
+ writeJson(join(source, '.workspaces', 'projects', 'nivo', 'fe.json'), {
73
+ $schema: '../../../.claude/readiness/initialization/workspaces/portable-route.schema.json',
74
+ schemaVersion: 6,
75
+ schemaRevision: 2,
76
+ project: 'nivo',
77
+ role: 'fe',
78
+ repository: {
79
+ kind: 'sibling', directory: 'nivo-fe', gitRepository: 'https://github.com/starci-lab/nivo-fe.git', branch: 'main',
80
+ gitPolicy: { mutationBranch: 'main', worktreeBranches: 'forbidden', incomingBranchRefs: 'merge-into-mutation-branch' }
81
+ },
82
+ context: {
83
+ instructions: [],
84
+ contract: 'packages/ui/src/contracts/index.ts',
85
+ contractSource: 'discovered:packages/ui/src/contracts/index.ts',
86
+ manifests: ['package.json'],
87
+ grammarId: 'core'
88
+ }
89
+ });
90
+ mkdirSync(join(source, '.workspaces', 'local', 'credentials'), { recursive: true });
91
+ writeFileSync(join(source, '.workspaces', 'local', 'credentials', 'provider.key.enc'), 'ciphertext');
92
+ return { repositories, source, frontend };
93
+ }
94
+
95
+ function runScript(fixture, ...args) {
96
+ return spawnSync(process.execPath, [script, ...args, '--source', fixture.source, '--repositories-root', fixture.repositories], {
97
+ encoding: 'utf8',
98
+ windowsHide: true
99
+ });
100
+ }
101
+
102
+ function runScriptWithEnv(fixture, env, ...args) {
103
+ return spawnSync(process.execPath, [script, ...args, '--source', fixture.source, '--repositories-root', fixture.repositories], {
104
+ encoding: 'utf8',
105
+ env: { ...process.env, ...env },
106
+ windowsHide: true
107
+ });
108
+ }
109
+
110
+ test('hydrates V1 and V6 routes, refreshes stale heads, and preserves credentials', (t) => {
111
+ const f = fixture(t);
112
+ let result = runScript(f, 'hydrate', '--apply');
113
+ assert.equal(result.status, 0, result.stderr);
114
+ const v1Path = join(f.source, '.workspaces', 'local', 'routes', 'source', 'be', 'config.json');
115
+ const v6Path = join(f.source, '.workspaces', 'local', 'routes', 'nivo', 'fe', 'config.json');
116
+ assert.equal(JSON.parse(readFileSync(v1Path, 'utf8')).version, 1);
117
+ const v6 = JSON.parse(readFileSync(v6Path, 'utf8'));
118
+ assert.equal(v6.schemaVersion, 6);
119
+ assert.equal(v6.schemaRevision, 2);
120
+ assert.equal(v6.context.contract, realpathSync(join(f.frontend, 'packages', 'ui', 'src', 'contracts', 'index.ts')));
121
+ assert.equal(v6.context.contractSource, 'discovered:packages/ui/src/contracts/index.ts');
122
+ assert.equal(v6.context.grammarId, 'core');
123
+ assert.deepEqual(v6.repository.gitPolicy, {
124
+ mutationBranch: 'main',
125
+ worktreeBranches: 'forbidden',
126
+ incomingBranchRefs: 'merge-into-mutation-branch'
127
+ });
128
+ assert.equal(v6.repository.head, git(f.frontend, 'rev-parse', 'HEAD'));
129
+ assert.equal(readFileSync(join(f.source, '.workspaces', 'local', 'credentials', 'provider.key.enc'), 'utf8'), 'ciphertext');
130
+
131
+ const unchanged = readFileSync(v6Path, 'utf8');
132
+ result = runScript(f, 'hydrate', '--apply');
133
+ assert.equal(result.status, 0, result.stderr);
134
+ assert.equal(readFileSync(v6Path, 'utf8'), unchanged);
135
+
136
+ writeFileSync(join(f.frontend, 'next.config.ts'), 'export default {};\n');
137
+ git(f.frontend, 'add', 'next.config.ts');
138
+ git(f.frontend, 'commit', '-m', 'advance');
139
+ result = runScript(f, 'check');
140
+ assert.equal(result.status, 1, result.stderr);
141
+ result = runScript(f, 'hydrate', '--apply');
142
+ assert.equal(result.status, 0, result.stderr);
143
+ assert.equal(JSON.parse(readFileSync(v6Path, 'utf8')).repository.head, git(f.frontend, 'rev-parse', 'HEAD'));
144
+ assert.equal(runScript(f, 'check').status, 0);
145
+ });
146
+
147
+ test('bootstrap clones a missing declared sibling and hydrates it without user-specific Git config', (t) => {
148
+ const f = fixture(t);
149
+ const bare = join(f.repositories, 'nivo-fe-origin.git');
150
+ execFileSync('git', ['clone', '--quiet', '--bare', f.frontend, bare], { windowsHide: true });
151
+ rmSync(f.frontend, { recursive: true, force: true });
152
+ const remote = 'https://github.com/starci-lab/nivo-fe.git';
153
+ const env = {
154
+ GIT_CONFIG_COUNT: '1',
155
+ GIT_CONFIG_KEY_0: `url.${pathToFileURL(bare).href}.insteadOf`,
156
+ GIT_CONFIG_VALUE_0: remote
157
+ };
158
+
159
+ const plan = runScriptWithEnv(f, env, 'bootstrap', '--plan', '--project', 'nivo');
160
+ assert.equal(plan.status, 0, plan.stderr);
161
+ assert.deepEqual(JSON.parse(plan.stdout).missingCheckouts, ['nivo-fe']);
162
+
163
+ const applied = runScriptWithEnv(f, env, 'bootstrap', '--apply', '--project', 'nivo');
164
+ assert.equal(applied.status, 0, applied.stderr);
165
+ assert.deepEqual(JSON.parse(applied.stdout).initializedCheckouts, ['nivo-fe']);
166
+ assert.equal(git(f.frontend, 'remote', 'get-url', 'origin'), remote);
167
+ assert.equal(runScript(f, 'check', '--project', 'nivo').status, 0);
168
+ });
169
+
170
+ test('current V6 rejects legacy grammar fields, mixed versions, and non-FE grammar while accepting the immediate predecessor', () => {
171
+ const base = {
172
+ $schema: '../../../.claude/readiness/initialization/workspaces/portable-route.schema.json',
173
+ schemaVersion: 6,
174
+ schemaRevision: 2,
175
+ project: 'nivo',
176
+ role: 'fe',
177
+ repository: {
178
+ kind: 'sibling', directory: 'nivo-fe', gitRepository: 'https://github.com/starci-lab/nivo-fe.git', branch: 'main',
179
+ gitPolicy: { mutationBranch: 'main', worktreeBranches: 'forbidden', incomingBranchRefs: 'merge-into-mutation-branch' }
180
+ },
181
+ context: {
182
+ instructions: [],
183
+ contract: null,
184
+ contractSource: null,
185
+ manifests: ['package.json'],
186
+ grammarId: 'core'
187
+ }
188
+ };
189
+ const preContractV6 = structuredClone(base);
190
+ delete preContractV6.schemaRevision;
191
+ delete preContractV6.context.contract;
192
+ delete preContractV6.context.contractSource;
193
+ assert.doesNotThrow(() => validatePortableRoute(preContractV6));
194
+ const pairedUnrevisionedV6 = structuredClone(base);
195
+ delete pairedUnrevisionedV6.schemaRevision;
196
+ assert.doesNotThrow(() => validatePortableRoute(pairedUnrevisionedV6));
197
+ assert.throws(() => validatePortableRoute({ ...base, version: 1 }), /invalid/);
198
+ assert.throws(() => validatePortableRoute({ ...base, context: { ...base.context, grammar: 'starci' } }), /invalid/);
199
+ assert.throws(() => validatePortableRoute({ ...base, role: 'be' }), /non-FE grammarId must be null|invalid/);
200
+ assert.throws(() => validatePortableRoute({
201
+ ...base,
202
+ context: { ...base.context, contract: 'packages/ui/src/contracts/index.ts', contractSource: null }
203
+ }), /invalid/);
204
+ assert.throws(() => validatePortableRoute({
205
+ ...base,
206
+ context: { ...base.context, contract: null, contractSource: 'discovered:packages/ui/src/contracts/index.ts' }
207
+ }), /invalid/);
208
+ assert.throws(() => validatePortableRoute({ ...base, repository: { ...base.repository, directory: '../nivo-fe' } }), /invalid/);
209
+ assert.throws(() => validatePortableRoute({ ...base, repository: { ...base.repository, gitRepository: 'https://token@github.com/starci-lab/nivo-fe.git' } }), /invalid/);
210
+ assert.throws(() => validatePortableRoute({
211
+ ...base,
212
+ repository: { ...base.repository, gitPolicy: { ...base.repository.gitPolicy, mutationBranch: 'feature/task' } }
213
+ }), /mutationBranch must equal repository\.branch/);
214
+ });
215
+
216
+ test('a session-only worktree policy hydrates and is kept verbatim', (t) => {
217
+ const f = fixture(t);
218
+ const declPath = join(f.source, '.workspaces', 'projects', 'nivo', 'fe.json');
219
+ const decl = JSON.parse(readFileSync(declPath, 'utf8'));
220
+ decl.repository.gitPolicy.worktreeBranches = 'session-only';
221
+ writeFileSync(declPath, JSON.stringify(decl, null, 2));
222
+ const result = runScript(f, 'hydrate', '--apply');
223
+ assert.equal(result.status, 0, result.stderr);
224
+ const v6 = JSON.parse(readFileSync(join(f.source, '.workspaces', 'local', 'routes', 'nivo', 'fe', 'config.json'), 'utf8'));
225
+ assert.equal(v6.repository.gitPolicy.worktreeBranches, 'session-only');
226
+ });
227
+
228
+ test('direct-main policy blocks hydration from a feature branch', (t) => {
229
+ const f = fixture(t);
230
+ git(f.frontend, 'switch', '-c', 'feature/task');
231
+ const result = runScript(f, 'hydrate', '--apply');
232
+ assert.equal(result.status, 1);
233
+ assert.match(result.stderr, /branch mismatch: expected main, observed feature\/task/);
234
+ });
235
+
236
+ test('a failing declaration produces no partial local route writes', (t) => {
237
+ const f = fixture(t);
238
+ const declaration = join(f.source, '.workspaces', 'projects', 'nivo', 'fe.json');
239
+ const value = JSON.parse(readFileSync(declaration, 'utf8'));
240
+ value.repository.branch = 'missing-branch';
241
+ writeJson(declaration, value);
242
+ const result = runScript(f, 'hydrate', '--apply');
243
+ assert.equal(result.status, 1);
244
+ assert.equal(existsSync(join(f.source, '.workspaces', 'local', 'routes', 'source', 'be', 'config.json')), false);
245
+ assert.equal(existsSync(join(f.source, '.workspaces', 'local', 'routes', 'nivo', 'fe', 'config.json')), false);
246
+ });